ReAct Agent实战:边想边查,根治大模型幻觉问题

发布时间:2026/10/20 20:29:40
ReAct Agent实战:边想边查,根治大模型幻觉问题
干AI应用开发这几年我一直觉得大模型一本正经地胡说八道是最劝退用户的现象。你问它一个事实问题它给你编一个有模有样的答案语气自信得像真查过资料一样。这个问题有专门的名字——幻觉hallucination。为了治这个毛病我试过换更大参数量的模型、试过各种提示词技巧但真正让我觉得路走通了的是 ReAct Agent 架构。这套架构让模型不再只靠内部记忆硬答而是像人一样先想、再查、后答。这篇文章我会从幻觉的根源讲起然后手把手带你实现一个完整的 ReAct Agent最后聊聊实测效果和我在落地时踩过的坑。全文不设门槛代码可以直接复制运行小白照着做也能跑起来。1. 幻觉问题模型为什么总是自信地胡编1.1 一个让我冷汗直流的Demo事故前两年我做一个内部知识库问答Demo测试阶段问了一条关于产品退换货政策的问题。当时模型给出的答案是支持7天无理由退换且运费由平台承担用户只需在订单页提交申请。这段话结构完整、语气笃定甚至贴心地补充了操作路径我第一反应是答得真好。后来核对原始文档才发现那条产品线根本不在无理由退换货范围内——答案里除了7天这个数字碰巧对上了其余全是模型自己拼接出来的。这件事给我刺激很大。不是气模型答错而是它答错时的自信程度让人完全放下防备。做应用的人都知道幻觉对用户体验的伤害是颠覆性的你推荐再准、交互再顺只要用户在一件关键事情上发现你说谎了整个系统的信任就塌了。幻觉不是锦上添花时的瑕疵而是决定产品竖不立得住的底线问题。1.2 拆开看幻觉的三个根源要解决幻觉得先搞清楚它从哪儿来。我自己的理解是三个原因叠加第一大模型本质上是个下一个词预测器。它学的是文本里的统计规律不是客观事实。训练时它看到苹果红色水果经常一起出现就学会在这些词之间建立强关联。但遇到冷门事实时它没有足够统计信号就只能按最通顺的方向生成。换句话说模型的目标不是说得对而是说得像。这是幻觉的底层原因。第二训练数据有截止时间也有覆盖盲区。昨天的新闻、某个产品的内部售后政策、某个小店的营业时间——这些东西要么不在训练数据里要么只占极小比例。模型不知道就是不知道但它在生成机制上没有任何主动承认不知道的倾向于是就会编。第三也是容易被忽视的一点模型生成答案后没有校验回路。人写完答案会复查一遍模型没有这个机制。你问它一个问题它生成完一整段回答这段回答自始至终没有经过任何外部事实的交叉验证。这就好比一个人闭卷考试交卷前不允许翻书、不允许对答案但他必须把每道题都填满。这三个根源决定了单纯换更大参数量的模型治不了幻觉单纯优化提示词也治不了。因为问题不在生成后怎么改而在生成过程中模型的信息来源太单一。要解决就必须让模型在回答时能主动获取外部信息——这就是 ReAct 的出发点。2. ReAct的思想拆解从背书机器到解决问题的人2.1 一句话理解ReAct边想边查查完再想ReAct 这个词来自学术圈一个经典思想是 Reasoning推理和 Acting行动的组合。它的核心主张非常朴素大模型推理时不应该只靠内部知识一路写到尾而应该在需要的时候动手——调用搜索、查询数据库、运行计算器——然后基于工具返回的真实结果继续推理。我做个类比你就明白了。以前的模型答题像闭卷考试一个学生对题目没把握也得硬着头皮写写错了也只能将错就错。ReAct 之后的模型答题像开卷考试学生可以先写下我这题缺个数据需要查一下然后翻书、按计算器看到真实结果后重新整理答案。开卷和闭卷的差距在信息密度高、时效性强的问题上是天壤之别。这个思想的价值在于它把模型的能力边界从内部参数里固化的知识扩展到了外部世界的一切可获取信息。2.2 和Chain-of-Thought的区别多了一步动手很多人会把 ReAct 和 Chain-of-Thought思维链混淆。两者都让模型输出中间推理过程但有个本质区别CoT 的中间过程只是脑内思考的显式化模型从头到尾没有接触任何新信息ReAct 的中间过程则真正允许模型中断生成去执行一次工具调用拿到工具反馈后再继续。举一个我实际遇到的例子。用户问某公司2025年营收同比增长了多少。用纯 CoT 的方式模型会想我需要知道2024年和2025年的营收然后用公式计算。——然后呢它没有2025年的数据因为训练数据可能只到2024年最后只能编一个数字。用 ReAct 的方式模型会在思考到缺数据这一步时停下调用查询工具获取两年真实营收再调用计算器算出增长率。同样是多步思考前者靠记忆后者靠证据可靠度完全不同。所以我对 ReAct 的评价是它不是对 CoT 的否定而是在 CoT 之上加了一个外部世界接口。思想再深也得有手有脚才算完整。2.3 核心循环Thought、Action、ObservationReAct 的运行机制可以简化成一个三步循环反复执行直到模型认为信息足够了Thought思考模型用自然语言描述它当前的判断比如我缺少2025年的营收数据需要先查询。Action行动模型选择一个工具并给出参数比如查询财务信息(某公司2025年营收)。Observation观察系统执行工具把真实结果返回给模型比如某公司2025年营收为120亿元。然后模型基于 Observation 进入下一轮 Thought循环往复当它觉得信息齐了就输出 Final Answer。这个循环最让我喜欢的一点是全程可追溯。模型哪一步想偏了、哪一次工具调用选错了都能看到。排查问题时比面对一个黑盒答案舒服太多了。我用一个具体例子展示完整循环。假设问题依然是某公司2025年营收同比增长了多少Thought需要知道2024年和2025年的营收数据。Action查询财务信息(某公司2024年营收)Observation某公司2024年营收为100亿元Thought还需2025年数据。Action查询财务信息(某公司2025年营收)Observation某公司2025年营收为120亿元Thought现在数据齐了计算增长率(120-100)/100×10020%。Action计算器((120-100)/100*100)Observation20.0Thought同比增长率为20%。Final Answer某公司2025年营收同比增长20%。从第1步到第11步每一步都有据可查。这就是 ReAct 的基本面貌——理论层讲清楚了下面进入实现。3. 从零实现一个ReAct Agent完整代码与运行效果3.1 环境准备你只需要三样东西动手之前先列清单。实现一个最小可用的 ReAct Agent你只需要Python 3.9 或更高版本装好requests如果需要调外部API。一个大模型聊天接口的访问权限。你可以用它原生的 SDK也可以封装一个llm.chat()函数。这篇文的代码里我会用抽象接口你自己替换成实际客户端即可。若干个工具函数。工具是 ReAct 的手下面我会定义两个一个模拟的时效信息查询工具一个计算器。提示工具函数不一定要接真实API。先把流程跑通再逐步替换成搜索服务、数据库查询、企业内网接口。建议第一次落地时都用 mock 工具调试成本低、速度快。3.2 定义工具给模型装上手工具分两层一层是给模型看的工具说明模型靠它决定何时调、怎么调一层是给代码执行的工具实现真实返回结果。先看我定义的 mock 工具def query_finance(topic: str) - str: 模拟财务信息查询工具 knowledge { 某公司2024年营收: 某公司2024年营收为100亿元, 某公司2025年营收: 某公司2025年营收为120亿元, 某公司2024年净利润: 某公司2024年净利润为15亿元, } return knowledge.get(topic, 未检索到相关信息) def calculator(expression: str) - str: 模拟计算器工具返回计算结果 try: # 生产环境不要直接eval建议用AST解析或专用计算库 result eval(expression) return f计算结果{result} except Exception as e: return f计算错误{e}这里我故意用eval简化代码但必须提醒你生产环境千万不能直接eval用户或模型传入的字符串存在严重安全风险。建议替换成ast.literal_eval或只允许白名单运算符的表达式解析器。工具的返回值尽量做成一句完整的话比如某公司2025年营收为120亿元而不是输出一堆 JSON 字段。原因后面第5章会细讲先记住模型对自然语言文本的处理能力远比对树状结构的处理能力稳定。3.3 提示词模板告诉模型怎么想怎么动手ReAct 的提示词是灵魂。它要做三件事定义可用工具、规定每一步的格式、说明何时可以停止。我用的模板长这样你是一个能够使用工具来回答问题的人工智能助手。 在回答过程中你必须使用以下循环格式 Thought: 你当前对问题的思考 Action: 工具名称从可用工具中选择 Action Input: 要传给工具的参数 当工具返回结果后继续输出 Thought直到你认为信息足够。 最终必须以如下格式结尾 Final Answer: 对用户的最终回答 可用工具 - query_finance: 查询指定公司或项目的历史财务数据。参数为查询主题字符串。 - calculator: 计算数学表达式。参数为合法的数学表达式如 (120-100)/100*100。注意几个细节每个工具说明要写清楚用途和参数格式格式约束要严格Action和Action Input成对出现;还要明确告知模型循环可以多轮直到信息足够。我没在模板里写死最多几轮这个由代码控制不给模型讨价还价的余地。3.4 主循环实现让模型真正跑起来主循环的职责是把模型输出解析成结构化动作执行工具把 Observation 追加回对话历史再让模型继续生成。import re def parse_response(text: str) - dict: 把模型输出解析为动作或最终答案 final_match re.search(rFinal Answer[:]\s*(.), text, re.S) if final_match: return {type: final, answer: final_match.group(1).strip()} action_match re.search(rAction[:]\s*(.), text) input_match re.search(rAction Input[:]\s*(.), text, re.S) if action_match and input_match: return { type: action, tool: action_match.group(1).strip(), input: input_match.group(1).strip(), } return {type: unknown, text: text} def execute_tool(tools: dict, tool_name: str, tool_input: str) - str: 执行工具返回观察结果 if tool_name not in tools: return f错误未知工具 {tool_name}可用工具为 {list(tools.keys())} try: return tools[tool_name](tool_input) except Exception as e: return f工具执行异常{e}然后是run_agent主循环def run_agent(question: str, tools: dict, max_steps: int 6): system_prompt build_react_prompt(tools) history [{role: user, content: f问题{question}}] for step in range(max_steps): response llm.chat(messagessystem_prompt history) history.append({role: assistant, content: response}) parsed parse_response(response) if parsed[type] final: return parsed[answer] if parsed[type] action: observation execute_tool(tools, parsed[tool], parsed[input]) history.append({role: user, content: fObservation: {observation}}) continue # 未知格式把提示反馈给模型让它自行修正 history.append({ role: user, content: 你的输出格式不正确请重新输出必须包含 Action 和 Action Input或 Final Answer。 }) return 达到最大步数仍未得到可靠答案。这里有个容易踩的坑Observation 是由系统产生的但我把它放在user角色里而不是assistant角色。原因是现在很多大模型接口对角色敏感你如果冒充assistant去插入一段它没说过的话模型可能直接混乱。实测用user角色携带 Observation 最稳。我还设置了最大步数max_steps6。这一步很重要——如果模型始终找不到答案循环会无限执行下去消耗大量 token。限制步数等于给流程装了个刹车。搭建完成看一次真实运行的日志。假设用户问的是某公司2025年营收同比增长了多少模型输出Thought: 我需要知道该公司2024年和2025年的营收先查2024年的。 Action: query_finance Action Input: 某公司2024年营收系统执行工具后把Observation: 某公司2024年营收为100亿元喂回去。模型继续Thought: 再查2025年的营收。 Action: query_finance Action Input: 某公司2025年营收拿到120亿元后Thought: 数据齐了现在计算增长率。 Action: calculator Action Input: (120-100)/100*100Observation 返回计算结果20.0模型输出Thought: 2025年营收相比2024年增长了20%。 Final Answer: 某公司2025年营收同比增长20%。整个过程一气呵成。你第一次跑通时可能会很兴奋——它居然真的知道去查数据了。但这只是开始真正的门道在下文的实测和踩坑里。4. 实测效果哪些幻觉被治好了哪些仍然顽固4.1 病根诊断先让模型裸答再上ReAct对比只看一个成功案例不足以服人。我习惯的做法是同一个问题先用纯模型不接工具回答再用 ReAct Agent 回答最后对照差异。先说一个事实性问题的实测。问题是某公司2025年全年营收是多少。纯模型回答某公司2025年全年营收约为90亿元相比去年有稳步增长。——这个答案最大的问题不是数字错实际上也是错的真实是120亿而是它用约为稳步增长这种模糊措辞掩盖了不确定性。模型不是不知道答案而是知道自己不知道但被语言习惯带偏了只能给出一个安全但不准确的表述。同一个问题交给 ReAct Agent流程是模型先 Thought我需要查询该公司2025年财务数据然后调query_financeObservation 直接返回某公司2025年营收为120亿元最终答案精确到单位。差距不是好一点而是有据可查和凭空捏造的差距。4.2 运算类问题模型不擅长算数但很擅长使用工具大模型的数学能力这几年进步不少但复杂一点的计算仍然容易翻车尤其是多位小数、百分比变化、汇率换算这类。我测过一个更刁钻的一件商品价格从80元涨到96元涨幅是多少个百分点纯模型可能绕半天给你个20.0%它算对了倒还好但一旦问题变成连续两次涨跌后的最终价格模型犯错的概率直线上升。ReAct 在这个场景的价值是它不需要自己算它能调用计算器。数学能力不行没关系借力打力。我测试时模型会在 Thought 阶段明确写我需要进行精确计算然后Action: calculatorObservation 返回精确结果最终答案零误差。这种知道自己该用工具的判断力比会做算式更稀缺。4.3 诚实的边界分析哪些幻觉ReAct治不了把话撂在这ReAct 不是万能药它在三类场景下依然会幻觉。第一工具本身返回错误信息。如果你的搜索接口返回的内容本身过期或有误模型基于错误观察继续推理得到的答案同样是错的。ReAct 只能保证答案有据可查没法保证据是准确的。第二模型在多轮推理中出现漂移。随着循环步数增加对话历史越来越长模型可能忘记最初的问题或者在多轮观察之间迷失方向。我遇到过模型查完 A 公司的数据后续 Thought 却套到 B 公司头上的情况。这是上下文注意力衰减导致的和幻觉机制同源。第三模型没有选对工具或参数。如果你提供的工具描述不清楚或者可用工具不匹配问题域模型会硬从工具列表里挑一个接近的然后基于错误输出硬编答案。工具设计不好ReAct 的幻觉率可能比纯模型还高。看清楚边界之后你应该明白ReAct 是降低幻觉概率的方案不是消灭幻觉的方案。它把幻觉从无据可查的编造变成了链路中某一环的错误。但即便如此它的价值依然巨大——因为链路中的错误是可发现、可修复的而编造是没法修的直接推翻。5. 进阶优化让Agent更可靠的关键经验5.1 工具设计三原则别让模型猜你的工具工具设计直接决定 Agent 的成败我的经验浓缩成三条第一工具说明里写清楚什么时候用、什么时候不用。比如query_finance的说明如果是查询财务数据模型面对这家公司口碑如何这种问题时也可能去调它。更稳妥的写法是仅用于查询营收、净利润等可公开获得的财务指标不用于评价公司声誉。限制越明确误用越少。第二参数数量能少则少。一个工具被期望接收三个参数时模型出错概率大幅上升。尽量把多参数封装成单字符串参数例如某公司2025年营收而不是company某公司, year2025, metricrevenue。字符串天然适合自然语言模型。第三返回值必须是完成的一句话。我最开始让工具返回 JSON 字典模型解析时经常出错改成某公司2025年营收为120亿元这种自然语言后准确率立刻上来了。工具返回的是给模型读的信息不是给程序解析的数据一定要按模型的阅读习惯来。5.2 错误处理让模型自己擦屁股工具可能失败比如查询无结果、API超时、计算出错。处理原则是不要把异常直接吞掉也不要让程序崩掉而是把错误信息作为 Observation 还给模型。举个例子模型调了calculator(120-)表达式不合法函数返回计算错误invalid syntax。模型看到这条 Observation 后通常会意识到自己参数传错了重新构造合法表达式再试一次。这种让模型从错误中自愈的方式比你在代码里硬编码重试逻辑灵活得多。当然自愈次数要限制。我的做法是同一个工具连续失败超过两次就把错误信息直接写入 Final Answer让用户知道系统查到这一步出了问题而不是让模型强行编一个答案。5.3 成本与延迟控制Agent不是越多步越好ReAct 的循环步数乘上模型调用次数token 开销确实比单次问答高不少。我自己的实际优化手段有四个限制最大步数。我生产环境一般设 4~6 步超过直接终止。大多数问题 3 步内就能解决少数复杂问题 5 步也够了。先用小模型做路由。收到问题时先让一个小模型判断这个问题是否需要工具需要哪个工具如果需要再走 Agent 流程不需要就直接用轻量模型回答。这样可以把简单问题的成本压下来。工具结果做截断。搜索或查询返回的内容可能很长我只保留前 200~500 字喂给模型防止上下文爆炸。缓存工具结果。相同 query 参数的工具结果直接复用这在知识库问答场景收益极大因为不同用户问的往往是同一批问题。5.4 我踩过的三个坑希望你绕开踩坑一提示词里没强制要求每步先写 Thought。结果模型跳过思考直接给 Action流程虽然能跑但一旦出问题完全无法定位是在哪一步想偏的。后来我在系统提示里加了每一步必须先输出 Thought后问题立刻改善。踩坑二正则解析太严格模型一换格式就解析失败。有的模型会输出Action: query_finance\nAction Input: 某公司2024年营收有的会输出Actionquery_finance查询财务数据。我在解析正则里加了冒号中英文兼容并对工具名做了模糊匹配解析失败率从5%降到0.2%以下。真实项目中建议再加一层解析失败时把反馈喂回给模型让它自己修正的兜底。踩坑三工具返回内容太啰嗦。我有一个查询工具返回3000字的文档片段模型经常被无关细节带偏甚至丢掉最初问题。后来我强制工具只返回直接与查询目标相关的段落幻觉率下降明显。工具返回的内容不是资料库而是刚好够模型下判断的那一口。我在实际落地中的体会是ReAct 不是银弹但它是目前性价比最高的一种幻觉治理手段。它没法让模型无所不知但能让模型在不知道的时候主动去查在查不到的时候老实承认。这个转变对做应用的人来说已经是从没法用到能用的关键一步。如果你正在被幻觉问题折磨别急着换模型先在模型外面套一层 ReAct 试试——大概率能让你少掉一半头发。