1. 为什么你需要亲手实现一个ReAct Agent?
如果你对AI Agent(智能体)感兴趣,或者已经用过了像AutoGPT、LangChain这类框架,你可能会觉得它们很强大,但同时又有点“黑盒”。你输入一个任务,它就开始自动搜索、思考、执行,最后给你一个答案。整个过程看起来很酷,但内部到底是怎么运转的?提示词长什么样?LLM(大语言模型)在每一步究竟接收到了什么信息?工具调用失败时,Agent会不会“卡住”?
这些问题,只有当你亲手从零开始构建一个Agent时,才能找到最清晰的答案。而ReAct(Reasoning + Acting) 范式,正是理解这一切的绝佳起点。它不像一些复杂框架那样封装了无数层抽象,它的核心思想极其简洁:让模型学会“三思而后行”。具体来说,就是让模型在解决问题时,交替进行思考(Thought)、行动(Action) 和观察(Observation)。思考用于分析当前情况和规划下一步;行动则是调用外部工具(比如搜索网络、查询数据库);观察是接收工具返回的结果,作为下一步思考的依据。这个循环会一直持续,直到模型认为已经得到足够信息,可以输出最终答案。
我刚开始接触Agent时,直接上手机器学习框架,虽然能跑起来,但总觉得隔着一层纱。后来我决定抛开框架,用最基础的Python,参照ReAct论文的原型,自己实现一遍。这个过程让我恍然大悟,原来Agent的核心逻辑可以如此清晰。你会发现,所谓的“智能”,很大程度上依赖于我们精心设计的提示词和可靠的工具调用循环。通过这次构建,你不仅能深刻理解ReAct,更能获得一种“造轮子”的自信,未来再去使用或定制任何Agent框架,你都能一眼看穿其本质。
所以,这篇文章就是带你走一遍我走过的路。我们将完全使用Python标准库和requests,不依赖任何重型框架,从零开始,一步步搭建一个能真实运行、具备搜索和查找能力的ReAct Agent。我会把每一行代码的逻辑、每一个设计选择背后的考量,以及我实际调试中踩过的坑,都毫无保留地分享给你。当你跟着完成时,你得到的不仅仅是一段可以运行的代码,更是一张深入AI Agent世界的清晰地图。
2. 拆解ReAct的核心循环:它到底是怎么“想”和“做”的?
在动手写代码之前,我们必须像建筑师看蓝图一样,彻底理解ReAct的工作流程。很多教程会直接扔给你一个循环图,但我想带你像侦探一样,拆解其中的每一个环节。整个流程的核心是一个受控的循环,Agent在这个循环里扮演一个“拥有工具的使用者”,而LLM则是它的“大脑”。
第一步:接收任务与初始化。 我们给Agent一个问题,比如“ChatGPT是哪个公司的?”。同时,我们要给Agent一个“工作手册”,也就是提示词模板。这个模板至关重要,它定义了Agent的“人格”和“能力范围”。它会告诉LLM:你现在是一个ReAct Agent,你可以进行三种操作(Search, Lookup, Finish),并且这是几个示例(Few-shot Learning)。我们把问题拼接到这个模板后面,就形成了给LLM的初始提示。
第二步:生成“思考”与“行动”。 这是循环的开始。我们将拼接好的提示词发送给LLM(比如通过OpenAI或国内的通义千问API)。这里的关键是,我们要求LLM严格按照“Thought X: ... Action X: ...”的格式输出。Thought部分是模型对当前情况的内部分析和推理,这部分内容不会直接影响外部世界,只是它的“内心戏”。Action部分则是它决定要对外部世界采取的具体操作。例如,它可能输出:“Thought 1: 我需要先知道ChatGPT是什么,然后才能查找它的公司。Action 1: Search[ChatGPT]”。
第三步:解析与执行行动。 我们的程序需要像老师批改作业一样,严格解析LLM返回的文本,准确提取出Thought和Action字符串。一旦提取出Action,比如Search[ChatGPT],程序就要调用对应的工具函数——这里是搜索维基百科的函数。这个函数会访问外部API或网页,获取关于“ChatGPT”的摘要信息。
第四步:形成“观察”并更新上下文。 工具函数返回的结果(比如一段描述文本)就成为了Observation。接下来是最精妙的一步:我们把“Thought”、“Action”和这个新的“Observation”作为一个完整的步骤,追加到最初的提示词后面。然后,整个流程回到第二步。现在,LLM收到的提示词包含了历史的所有步骤(Thought1, Action1, Observation1),它在此基础上进行新一轮的思考。例如,它可能看到Observation1里提到了“OpenAI”,那么它下一步的Thought2可能就是“ChatGPT由OpenAI创建,那么我需要搜索OpenAI来了解其子公司和产品”,Action2则可能是Search[OpenAI]。
第五步:循环终止。 这个“思考-行动-观察”的循环会一直进行,直到模型生成的Action是Finish[最终答案]。此时,程序跳出循环,提取出Finish[]括号中的内容作为最终答案返回。我们通常也会设置一个最大步数(比如8步),防止问题过于复杂导致无限循环。
这个循环的巧妙之处在于,它将模型的推理过程(Thought)外部化、结构化,使得整个决策过程变得可追踪、可调试。同时,通过将工具调用结果(Observation)反馈给模型,实现了与外部世界的闭环交互。你之后会发现,无论多复杂的Agent框架,其底层都离不开这个基本的交互循环。
3. 搭建你的开发环境与工具函数
理论清楚了,我们开始动手。首先确保你有一个Python环境(3.8以上即可)。我们不需要TensorFlow或PyTorch,只需要几个轻量级的库来处理网络请求和HTML解析。打开你的终端,创建一个新的项目目录,然后安装依赖:
pip install requests beautifulsoup4
requests库用于调用LLM API和访问维基百科,beautifulsoup4则用来解析维基百科返回的HTML页面,提取我们需要的文本内容。接下来,我们创建一个Python文件,比如叫做react_agent.py。文件开头,我们先导入必要的模块:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
从零构建ReAct Agent
一个纯Python实现,用于深入理解思考-行动-观察循环的核心机制。
"""
import requests
import time
import re
import json
from bs4 import BeautifulSoup
现在,我们来配置最关键的部分——LLM的访问。为了代码的通用性,我们使用与OpenAI API兼容的接口格式。这里我以阿里云的通义千问为例,因为它提供了兼容的端点,且易于获取API Key。你也可以替换成任何支持相同接口的模型服务。
# =============================================================================
# 配置部分:请替换为你自己的API信息
# =============================================================================
OPENAI_API_KEY = "你的API-KEY" # 例如:sk-xxxxxxxx
OPENAI_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions" # 通义千问兼容端点
MODEL_NAME = "qwen-turbo" # 或其他你拥有的模型名,如 qwen-max
注意:请务必妥善保管你的API Key,不要上传到公开的代码仓库。你可以从环境变量中读取,这里为了演示清晰直接写在代码里。接下来,我们实现LLM的调用函数。这个函数是整个Agent的“大脑”接入点:
def llm(prompt, stop=None, max_tokens=200, temperature=0):
"""调用LLM API,返回生成的文本。"""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {OPENAI_API_KEY}"
}
payload = {
"model": MODEL_NAME,
"messages": [{"role": "user", "content": prompt}],
"temperature": temperature, # 设为0使输出更确定,有利于格式解析
"max_tokens": max_tokens
}
if stop:
payload["stop"] = stop # 停止序列,用于控制生成长度
try:
resp = requests.post(OPENAI_BASE_URL, headers=headers, data=json.dumps(payload), timeout=60)
resp.raise_for_status() # 如果状态码不是200,抛出异常
data = resp.json()
# 从标准兼容格式中提取返回内容
if "choices" in data and len(data["choices"]) > 0:
return data["choices"][0]["message"]["content"].strip()
else:
print("警告:LLM返回格式异常")
return ""
except requests.exceptions.Timeout:
print("错误:LLM API请求超时")
return ""
except Exception as e:
print(f"LLM调用错误: {e}")
return ""
这个函数封装了与API的交互。stop参数非常重要,我们后面会用它来告诉LLM在生成完Action后及时停止,不要继续胡言乱语。temperature=0是为了让模型输出尽可能稳定,便于我们后续做格式解析。
4. 构建Agent的“手”:实现搜索与查找工具
Agent有了“大脑”(LLM),还需要“手”来操作外部世界。在经典的ReAct示例中,这双手就是访问维基百科的工具。我们来实现两个核心工具函数:Search和Lookup。
Search[实体]工具负责去维基百科上搜索一个实体(比如“OpenAI”),并返回第一段摘要信息。如果该实体没有直接对应的页面,则返回一些相似的搜索建议。这里涉及到网络爬虫的基本技巧,我们使用requests获取页面,再用BeautifulSoup解析HTML。
def clean_str(text):
"""清理文本中的编码问题。"""
try:
return text.encode().decode("unicode-escape").encode("latin1").decode("utf-8")
except:
return text
def search_wikipedia(entity):
"""搜索维基百科实体,返回页面全文和观察摘要。"""
# 构造搜索URL
search_url = f"https://en.wikipedia.org/w/index.php?search={entity.replace(' ', '+')}"
try:
response = requests.get(search_url, timeout=10)
soup = BeautifulSoup(response.text, "html.parser")
# 情况1:搜索结果是列表页(即没有直接匹配的页面)
result_divs = soup.find_all("div", {"class": "mw-search-result-heading"})
if result_divs:
result_titles = [clean_str(div.get_text().strip()) for div in result_divs[:5]]
return None, f"找不到精确匹配的[{entity}]。相似的条目有:{result_titles}。"
# 情况2:直接跳转到了条目页面(或消歧义页面)
page_elements = soup.find_all("p") + soup.find_all("ul")
page_texts = [p.get_text().strip() for p in page_elements]
# 处理消歧义页面(例如搜索“Python”可能指向编程语言或蛇)
if any("may refer to:" in p for p in page_texts):
# 如果是消歧义页,我们可以递归搜索一个更具体的条目,这里简单返回提示
return None, f"[{entity}]可能指代多个事物,请提供更具体的名称。"
# 提取页面主要内容,拼接成全文
page_content = ""
for p in page_texts:
if len(p.split(" ")) > 2: # 过滤掉过短的段落(可能是导航信息)
page_content += clean_str(p) + "\n"
# 生成一个简短的观察摘要(取前5个句子)
paragraphs = [p.strip() for p in page_content.split("\n") if p.strip()]
sentences = []
for p in paragraphs:
sentences += [s.strip() + '.' for s in p.split('. ') if s.strip()]
obs = ' '.join(sentences[:5])
return page_content, obs
except Exception as e:
return None, f"网络搜索出错:{str(e)}"
Lookup[关键词]工具则用于在Search获取到的当前页面全文(current_page)中,查找包含特定关键词的句子。这里有一个设计难点:页面内容可能很长,而LLM的上下文有限,我们不能每次都把全文喂给它。所以Lookup需要实现一个“迭代查找”的功能,每次调用返回下一个匹配的句子,并记录查找状态。
def lookup_in_page(page, keyword, lookup_state):
"""在页面文本中查找关键词,返回下一个匹配的句子及其状态。"""
if not page:
return "没有页面内容可以查找。", lookup_state
# 如果关键词变了,或者首次查找,需要重新搜索所有句子
if lookup_state.get('keyword') != keyword:
paragraphs = [p.strip() for p in page.split("\n") if p.strip()]
all_sentences = []
for p in paragraphs:
all_sentences += [s.strip() + '.' for s in p.split('. ') if s.strip()]
# 过滤出包含关键词的句子(不区分大小写)
matched_sentences = [s for s in all_sentences if keyword.lower() in s.lower()]
lookup_state['keyword'] = keyword
lookup_state['results'] = matched_sentences
lookup_state['index'] = 0 # 重置指针
# 检查是否还有结果
if lookup_state['index'] >= len(lookup_state['results']):
return "没有更多匹配的结果了。", lookup_state
# 返回当前结果,并移动指针
result = lookup_state['results'][lookup_state['index']]
obs = f"(结果 {lookup_state['index'] + 1} / {len(lookup_state['results'])}) {result}"
lookup_state['index'] += 1
return obs, lookup_state
我在这里踩过一个坑:最初没有设计lookup_state这个状态字典,导致每次Lookup都只能返回第一个匹配项,Agent无法深入阅读。加入状态记录后,Agent就能像我们手动按“下一个”一样,逐步浏览所有相关句子,这对于提取分散的信息至关重要。
5. 设计Agent的“工作手册”:提示词工程详解
现在到了最核心,也是最体现技巧的部分——构造提示词。你可以把提示词想象成给LLM的一份详细“工作手册”和“岗前培训材料”。它必须清晰定义任务、格式、可用工具,并提供高质量示例。我们的提示词模板REACT_PROMPT是一个长字符串,我把它拆解开来给你看:
第一部分:任务指令。 开宗明义,告诉模型要扮演的角色和行为模式。
解决问答任务时需要交替进行"思考(Thought)"、"行动(Action)"和"观察(Observation)"的步骤。
"思考"可以用来推理当前情况;"行动"可以是以下三种类型:
(1)Search[实体]:在维基百科上搜索该确切实体,如果存在则返回第一段文字;如果不存在,则返回一些相似的实体以供继续搜索。
(2)Lookup[关键词]:在当前文章中返回包含该关键词的下一句。
(3)Finish[答案]:返回最终答案并结束任务。
这部分必须清晰无歧义。我特别强调了Search和Lookup的区别:Search是去外部获取新知识,Lookup是在已有知识(当前页面)里精读。
第二部分:少样本示例(Few-shot Examples)。 这是提示词成功的关键。我们提供了2-3个完整的问答示例,展示了从问题开始,到最终Finish的完整推理轨迹。示例必须覆盖不同的情况,比如:
- 需要多步搜索和查找的复杂问题(如“Colorado造山运动...”)。这个示例展示了如何通过
Search定位主题,再通过Lookup在页面中挖掘细节,最后串联信息得到答案。 - 需要从文本中提取命名关系的直接问题(如“Milhouse是以谁命名的?”)。这个示例展示了当第一次
Search结果不包含答案时,如何使用Lookup[named after]来找到关键句。 - 需要比较和排除的选择题(如“哪一部纪录片是关于芬兰摇滚乐队的...”)。这个示例展示了
Search失败(找不到精确匹配)时,如何根据搜索结果进行推理和排除。
每个示例都严格遵循“Question: ... Thought 1: ... Action 1: ... Observation 1: ...”的格式。模型会从这些示例中学习到正确的推理节奏和格式规范。我建议你在自己的实践中,根据你的任务领域,精心构造这样的示例,这比调任何参数都管用。
最后一部分:拼接新问题。 在运行时,我们会把用户的新问题拼接到这个固定模板的后面,形成最终的提示词:
prompt = REACT_PROMPT + f"\nQuestion:{question}\n"
这样,模型就能基于“工作手册”和“历史优秀案例”,开始处理新的任务了。
6. 实现主循环与解析器:让Agent“动”起来
有了大脑、双手和工作手册,现在我们需要一个“调度中心”来协调一切。这就是react_solve主函数和parse_thought_action解析器。主函数控制整个ReAct循环的流程,而解析器则确保我们能准确理解LLM的“输出”。
我们先看解析器。LLM的输出并不总是完美的,有时它可能会重复前缀,或者格式略有偏差。一个健壮的解析器必须能处理这些小毛病。
def parse_thought_action(text, step_num):
"""从LLM返回的文本中解析出第step_num步的Thought和Action。"""
text = text.strip()
thought_prefix = f"Thought {step_num}:"
action_prefix = f"Action {step_num}:"
# 处理可能重复的前缀
if text.startswith(thought_prefix):
text = text[len(thought_prefix):].strip()
# 方法1:优先使用换行符分割(最规范的格式)
if f"\n{action_prefix}" in text:
parts = text.split(f"\n{action_prefix}", 1)
thought = parts[0].strip()
action = parts[1].strip()
# 方法2:如果没有换行,尝试直接找Action前缀
elif action_prefix in text:
parts = text.split(action_prefix, 1)
thought = parts[0].strip()
action = parts[1].strip()
# 方法3:使用正则表达式作为兜底
else:
action_pattern = rf"Action\s*{step_num}\s*[::]\s*(.+)"
action_match = re.search(action_pattern, text, re.IGNORECASE)
if action_match:
action = action_match.group(1).strip()
thought = text[:action_match.start()].strip()
else:
# 如果都解析失败,可能LLM只输出了Thought
thought = text
action = ""
# 最后,清理thought中可能残留的标记
thought = re.sub(rf"^(Thought\s*{step_num}\s*[::]\s*)+", "", thought, flags=re.IGNORECASE).strip()
return thought, action
接下来是核心的主循环函数react_solve。它初始化状态(当前页面、查找状态),然后进入一个for循环,最多执行max_steps步。
def react_solve(question, max_steps=8, verbose=True):
"""使用ReAct范式解决问题,返回答案和完整的推理轨迹。"""
current_page = None # 保存当前搜索到的页面全文
lookup_state = {'keyword': None, 'results': [], 'index': 0}
answer = None
if verbose:
print(f"\n🔍 开始处理问题:{question}")
print("=" * 60)
# 1. 构建初始提示词
prompt = REACT_PROMPT + f"\nQuestion:{question}\n"
for step in range(1, max_steps + 1):
# 2. 调用LLM生成Thought和Action
# 注意stop参数:我们告诉LLM,生成到“Observation X:”或下一个“Thought”之前就停止
thought_action_text = llm(
prompt + f"Thought {step}:",
stop=[f"\nObservation {step}:", f"\nThought {step+1}:", "\nQuestion:"],
max_tokens=250
)
# 3. 解析输出
thought, action = parse_thought_action(thought_action_text, step)
# 4. 根据Action类型执行工具调用,得到Observation
obs = ""
if action.startswith("Search[") and action.endswith("]"):
entity = action[len("Search["):-1]
current_page, obs = search_wikipedia(entity)
# 执行了新的Search,旧的Lookup状态需要重置
lookup_state = {'keyword': None, 'results': [], 'index': 0}
elif action.startswith("Lookup[") and action.endswith("]"):
keyword = action[len("Lookup["):-1]
obs, lookup_state = lookup_in_page(current_page, keyword, lookup_state)
elif action.startswith("Finish[") and action.endswith("]"):
answer = action[len("Finish["):-1]
obs = "任务完成。"
if verbose:
print(f"✅ 在第{step}步获得最终答案。")
else:
obs = f"无效动作格式:'{action}'。请使用Search[实体]、Lookup[关键词]或Finish[答案]。"
# 5. 将本轮步骤记录到提示词中,形成历史上下文
step_record = f"Thought {step}:{thought}\nAction {step}:{action}\nObservation {step}:{obs}\n"
prompt += step_record
if verbose:
print(f"**步骤 {step}**")
print(f" 思考: {thought}")
print(f" 行动: {action}")
print(f" 观察: {obs[:150]}...") if len(obs) > 150 else print(f" 观察: {obs}")
print("-" * 40)
# 6. 如果得到最终答案,提前结束循环
if answer:
break
# 循环结束,处理结果
if not answer:
answer = "经过最大步数尝试,仍无法确定答案。"
if verbose:
print("⚠️ 达到最大步数限制,未找到答案。")
if verbose:
print("=" * 60)
print(f"💡 最终答案:{answer}")
return {'answer': answer, 'trajectory': prompt}
这个函数里有几个我调试后留下的重要细节:第一,在调用llm时,stop参数设置了f”\nObservation {step}:“,这能非常有效地防止LLM“抢答”,自己把Observation也编出来。第二,每次执行完Search,我都会重置lookup_state,因为页面已经换了,之前的查找状态自然失效。第三,verbose参数让整个过程可视化,这对于调试和理解Agent行为不可或缺。
7. 运行、测试与调试你的第一个Agent
代码都写好了,是时候见证它的工作了。在文件末尾,我们添加一个测试入口:
if __name__ == "__main__":
# 测试几个不同复杂度的问题
test_questions = [
"ChatGPT是哪个公司开发的?",
"《哈利波特》的作者J.K.罗琳是哪国人?",
"特斯拉汽车的CEO埃隆·马斯克还创办了哪家太空探索公司?",
]
for q in test_questions:
result = react_solve(q, max_steps=6, verbose=True)
print("\n" + "🌟" * 30 + "\n")
time.sleep(2) # 避免请求过于频繁
运行这个脚本(python react_agent.py),你会看到终端里打印出Agent完整的思考过程。以第一个问题为例,你可能会看到类似这样的输出:
🔍 开始处理问题:ChatGPT是哪个公司开发的?
============================================================
**步骤 1**
思考: 我需要知道ChatGPT是什么,以及它是哪个公司创建的。
行动: Search[ChatGPT]
观察: ChatGPT是由OpenAI开发的人工智能聊天机器人。OpenAI是一家美国人工智能研究实验室...
----------------------------------------
**步骤 2**
思考: 从观察中得知ChatGPT由OpenAI开发。问题问的是“哪个公司”,所以答案就是OpenAI。
行动: Finish[OpenAI]
观察: 任务完成。
----------------------------------------
✅ 在第2步获得最终答案。
============================================================
💡 最终答案:OpenAI
这个过程非常直观!Agent首先思考需要搜索“ChatGPT”,然后执行搜索,观察结果得知是OpenAI,接着思考这已经回答了问题,于是果断结束任务。对于更复杂的问题,比如问马斯克的其他公司,Agent可能会先搜索“Elon Musk”,观察结果中找到“SpaceX”,然后可能再搜索“SpaceX”进行确认,最后才给出答案。
调试经验分享:你第一次运行很可能不会这么顺利。常见的问题有:
- API调用失败:检查API Key、Base URL和网络连接。给
llm函数添加更详细的错误打印。 - 解析失败:LLM没有严格按照格式输出。检查你的
stop参数是否生效,或者考虑在提示词中更严厉地强调格式。可以临时打印出thought_action_text原始文本来分析。 - 工具调用无结果:维基百科可能被屏蔽或访问慢。可以考虑增加超时时间,或者替换为其他知识源(如国内百科的API,但需要注意接口格式适配)。
- Agent陷入循环:比如不停地
Lookup同一个词。可以在主循环里加入简单的逻辑判断,如果连续三步的Action都一样,则强制干预,返回一个提示观察。
调试的过程,正是你与Agent心智模型对话的过程,也是理解其局限性的最好方式。
8. 超越基础:优化方向与扩展思考
一个能跑起来的基础ReAct Agent已经完成了。但这就是终点吗?远远不是。这只是一个起点,从这里出发,你有无数个可以优化和扩展的方向,让这个Agent变得更强大、更智能。
首先,优化工具集。 我们只实现了维基百科搜索。一个实用的Agent可能需要连接更多工具:
- 计算器:处理数学问题。可以调用
eval(需极度注意安全性)或math库。 - 天气查询:接入天气API。
- 数据库查询:连接SQLite或MySQL,让Agent能回答关于私有数据的问题。
- 代码执行:提供一个安全的沙箱环境执行Python代码片段(例如使用
docker隔离或restrictedpython)。 每增加一个工具,你都需要在提示词的“行动”部分更新说明,并提供新的使用示例。
其次,增强推理与规划能力。 基础的ReAct是“走一步看一步”。我们可以引入更高级的策略:
- 子目标分解:对于复杂问题,在第一步思考时,就要求模型先制定一个计划(Plan),列出几个子目标,然后逐步执行。
- 自我反思:在得到Observation后,增加一个“Reflection”步骤,让模型评估当前信息是否足够、行动是否有效,必要时调整策略。
- 长上下文管理:随着步数增加,提示词会越来越长。我们需要设计摘要机制,将过去的步骤压缩成精炼的要点,以节省Token并聚焦关键信息。
第三,提升可靠性与用户体验。
- 错误处理与重试:网络工具调用失败时,不应直接让Agent“卡死”,而应该让模型思考备选方案或重试。
- 置信度与溯源:让Agent在给出答案时,附带引用来源(例如,来自Search[OpenAI]页面的第X句)。这对于需要核查的场合非常重要。
- 人机交互:将单次问答扩展成对话式,记住历史对话上下文。
最后,关于性能与成本。 每一步都需要调用LLM,成本可能很高。在实际应用中,我们需要权衡步数限制与问题复杂度。对于一些简单问题,完全可以设计一个“短路”机制,让模型判断能否直接回答,无需调用工具。
我把自己实现的这个基础版本看作一个“教学原型”。它剥离了所有复杂性,让你能亲手触摸到ReAct的每一个齿轮。当你理解了这些,再去学习LangChain的ReActAgent或AutoGPT的复杂架构,你会发现它们无非是在这个核心循环之上,增加了更多工具、更优的记忆管理、更漂亮的工程封装而已。

5751

被折叠的 条评论
为什么被折叠?



