LangChain集成ReAct实现高可靠AI Agent的工程实践
1. 项目概述:当LangChain遇上ReAct,不是“加法”,而是“重写执行逻辑”
你有没有试过用LangChain搭一个能查天气、能搜新闻、还能算账的AI助手,结果发现它动不动就卡在“思考”环节,要么反复调用同一个工具不收敛,要么干脆编造答案还振振有词?我去年带三个实习生做智能客服中台时,就栽在这上面——明明用了最新开源的LLM,API响应时间不到800ms,可整个Agent链路跑下来平均要4.2秒,用户等得不耐烦直接挂断。后来我们把日志拉出来一帧帧看,才发现问题根本不在模型本身,而在于LangChain默认的Agent执行范式:它把“思考-选工具-执行-总结”当成线性流水线,但真实世界的问题从来不是单步推理能解的。ReAct(Reasoning + Acting)方法的出现,本质上不是给LangChain加了个新模块,而是把它的执行引擎从“脚本式调度”升级成了“认知闭环系统”。它强制模型在每一步都输出明确的推理链(Thought)、动作指令(Action)、动作参数(Action Input)和观测结果(Observation),四者缺一不可。这就像给AI装上了“操作日志+回溯指针”,既能让开发者看清它到底卡在哪一步,也能让模型自己基于上一轮观测修正下一轮推理。本文不讲论文复述,只说我们踩坑后实测有效的落地方案:如何用最少的代码改动,把一个原本响应迟钝的LangChain Agent,变成能在3秒内完成多跳查询、跨工具协同、错误自恢复的稳定服务。适合所有正在用LangChain做生产级应用的工程师、技术负责人,以及想真正搞懂Agent底层逻辑的进阶学习者——如果你还停留在“chain.run()就能跑通”的阶段,这篇内容会直接刷新你对LangChain效率边界的认知。
2. 核心设计思路拆解:为什么ReAct不是“可选项”,而是“必选项”
2.1 LangChain默认Agent的三大隐性瓶颈
LangChain官方文档里把Agent描述成“LLM与外部工具的桥梁”,这个比喻很美,但掩盖了实际工程中的硬伤。我们团队用ZeroShotAgent和ConversationalAgent跑了三个月线上流量,最终归结出三个无法绕开的效率黑洞:
第一是工具选择的模糊性。默认Agent依赖LLM直接输出工具名(如"weather_api"),但大模型在token压力下常犯两类错:一是缩写工具名("weath")、二是拼错("wheather_api"),导致ToolNotFoundException频发。我们统计过,线上23%的失败请求源于此。更糟的是,LangChain的错误处理机制是“抛异常→终止→返回报错”,没有重试或降级逻辑。而ReAct强制要求模型输出结构化Action指令(如Action: weather_api),配合正则预校验,能把工具名误识别率压到0.7%以下。
第二是推理过程的不可见性。传统Agent的intermediate_steps只记录“调了什么工具、返回了什么”,但不记录“为什么调这个工具”。比如用户问“上海明天会不会下雨,如果会,带伞吗?”,模型可能先查天气,再查交通,最后才回答——可你根本不知道它查交通的动机是什么。ReAct的Thought字段就是为解决这个问题:它必须显式写出推理依据(如“需要确认地铁是否因暴雨停运,以便建议出行方式”),这不仅方便调试,更让后续的Prompt Engineering有了锚点——我们可以基于Thought内容动态注入领域知识,而不是盲目堆参数。
第三是状态管理的脆弱性。默认Agent把整个对话历史塞进prompt,随着轮次增加,token消耗指数级上升。我们测试过,当对话超过7轮,ConversationBufferMemory的prompt长度就突破3200token,触发LLM的上下文截断,导致模型“失忆”。ReAct通过将Observation作为独立字段注入下一轮,天然实现状态压缩:你只需传入最新一轮的Thought+Action+Observation,而非全部历史。实测显示,在同等对话深度下,ReAct模式的prompt平均缩短41%,直接降低35%的API成本。
提示:别被“ReAct是新算法”的说法误导。它本质是约束式Prompt Engineering——用格式规范倒逼模型暴露内部推理过程。LangChain的
ReActAgent类只是封装了这个规范,真正的效率提升来自你如何设计Thought的引导语、如何校验Action的合法性、如何处理Observation的噪声。
2.2 ReAct Agent与LangChain原生Agent的架构级差异
很多人以为切换Agent类型只是改一行代码(agent=ReActAgent(...)),但实际涉及整个执行流的重构。我们画了一张对比图(文字版),说明关键差异点:
| 维度 | LangChain默认Agent(ZeroShot) | ReAct Agent |
|---|---|---|
| 输入构造 | 将全部工具描述+对话历史拼接为长prompt | 工具描述静态加载,每轮仅注入最新Thought+Observation |
| 输出解析 | 正则匹配"Action:"后的内容,无结构校验 | 强制解析四元组(Thought/Action/Action Input/Observation),缺失任一字段即报错 |
| 错误处理 | 工具调用失败→中断流程→返回错误字符串 | 工具调用失败→生成Observation:“调用weather_api失败,错误码503”→模型基于此重新推理 |
| 状态传递 | Memory对象维护完整对话历史,逐轮追加 | 每轮仅保留上一轮Observation,Thought由模型实时生成,无历史包袱 |
| 调试粒度 | 只能看到“第3轮调用weather_api返回{...}” | 能看到“第3轮Thought:需验证降雨概率是否>70%→Action:weather_api→Action Input:{city: 'shanghai'}→Observation:{rain_prob: 85%}” |
这个差异直接决定了工程复杂度。默认Agent的调试像在黑盒里听声辨位,而ReAct Agent的调试像看着手术直播——你能精准定位到模型哪一步推理出了偏差。比如我们曾发现模型在处理“比较两个城市温度”时,总在Action Input里漏掉第二个城市参数。问题不是模型能力不足,而是Prompt里没强调“比较类问题必须提供两个city参数”。ReAct的结构化输出让我们快速定位到Prompt缺陷,两天就修复了。
2.3 为什么不用LangChain内置的ReActAgent?我们自研封装的三大理由
LangChain确实提供了ReActAgent类,但我们在线上环境弃用了它,转而基于AgentExecutor和自定义AgentOutputParser重写。原因有三:
第一是工具调用的原子性失控。官方ReActAgent在解析到Action: search后,会直接调用search.run(),但真实业务中,搜索工具往往需要鉴权token、限流控制、缓存策略。我们无法在run()方法里注入这些逻辑。而自研方案把工具调用抽象为ToolExecutor类,统一处理重试、熔断、日志埋点,比如search工具的执行逻辑实际是:
def execute_search(query: str) -> str: if cache.get(query): return cache.get(query) try: response = requests.get(f"{SEARCH_API}/?q={query}", headers={"X-Token": get_token()}) response.raise_for_status() result = response.json()["data"][:3] # 只取前三条 cache.set(query, result, expire=300) return json.dumps(result) except Exception as e: logger.error(f"Search failed for {query}: {e}") return f"Search tool unavailable. Error: {str(e)}"第二是Observation的噪声过滤缺失。官方版本把工具原始返回值(可能是HTML、XML、冗长JSON)全量塞进Observation,导致LLM被无关信息干扰。我们强制所有工具返回标准化字典,再由ObservationFormatter清洗:
# 原始天气API返回 {"location": {"name": "Shanghai"}, "current": {"temp_c": 22, "condition": {"text": "Sunny"}}} # 经ObservationFormatter处理后 "Shanghai current temperature is 22°C, weather condition: Sunny"第三是超时控制的粗暴性。官方Agent设置max_iterations=15,但实际中某次天气API慢了8秒,模型还在等Observation,整个请求卡死。我们给每个工具调用加了独立超时(search: 3s,weather: 2s,calculator: 0.5s),超时后自动注入Observation: "Tool timeout, use default value",保证流程不阻塞。
注意:自研不等于重复造轮子。我们90%的代码复用LangChain的
Tool、LLMChain、AgentExecutor,只是把ReActOutputParser和ToolExecutor换成可控版本。这种“微内核+插件化”设计,让我们在保持LangChain生态兼容的同时,获得了生产级稳定性。
3. 核心细节与实操要点:从零搭建高效率ReAct Agent
3.1 工具设计原则:不是“能用就行”,而是“必须可控”
ReAct Agent的效率上限,首先取决于工具的设计质量。我们制定了三条铁律,违反任一条都会引发连锁故障:
铁律一:工具必须幂等且无副作用。这是最容易被忽视的点。比如设计一个“发送邮件”工具,如果每次调用都真发邮件,那模型一旦陷入循环调用(常见于Thought逻辑错误),就会造成严重事故。我们的解决方案是:所有工具默认为“dry-run”模式,只有当Action Input中明确包含"confirm": true时才执行真实操作。例如:
def send_email_tool(to: str, subject: str, body: str, confirm: bool = False) -> str: if not confirm: return f"Dry-run: Would send email to {to} with subject '{subject}'" # 真实发送逻辑...这样即使模型误判,最多只产生日志,不会影响业务。
铁律二:工具返回必须结构化且可预测。拒绝任何自由文本输出。我们要求所有工具返回Python字典,且字段名固定(status,data,error)。比如计算器工具:
def calculator_tool(expression: str) -> dict: try: # 安全计算,禁用eval,用ast.literal_eval result = ast.literal_eval(expression) return {"status": "success", "data": result, "error": None} except Exception as e: return {"status": "error", "data": None, "error": str(e)}这样ObservationFormatter才能稳定提取data字段,避免模型被"Error: invalid syntax"这类字符串干扰。
铁律三:工具必须自带熔断与降级。我们用tenacity库实现工具级熔断:
from tenacity import retry, stop_after_attempt, wait_exponential @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10) ) def weather_api_call(city: str) -> dict: # 实际API调用 pass同时配置降级策略:当熔断触发时,返回预设的Observation: "Weather service degraded, using historical average",确保Agent流程不中断。
实操心得:我们曾因忽略“幂等性”栽过跟头。某次模型在处理“取消订单”请求时,因Thought逻辑错误反复调用
cancel_order工具,导致用户订单被取消三次。后来我们强制所有变更类工具加confirm参数,并在数据库加唯一索引(order_id + action_timestamp),才彻底解决。
3.2 Prompt工程:让模型“学会思考”,而不是“猜思考”
ReAct的核心是Thought字段,但很多团队直接套用LangChain默认Prompt,效果极差。我们花了两周时间做A/B测试,最终确定了高效Thought生成的四大要素:
要素一:明确Thought的边界。默认Prompt只说“请先思考”,但模型不清楚思考该覆盖多大范围。我们在Prompt中明确定义:
“Thought必须严格满足:① 解释当前问题的核心需求;② 列出为满足需求必须获取的1-3个事实;③ 说明下一步Action如何获取这些事实。禁止包含工具调用细节、参数猜测、结果预测。”
这条规则让Thought质量提升显著。以前模型常写“我将调用weather_api查询上海天气”,现在会写“用户需知道上海明日降雨概率以决定是否带伞,需获取:1) 上海明日最高温;2) 降雨概率;3) 风速等级。因此调用weather_api”。
要素二:注入领域知识锚点。通用LLM对垂直领域术语理解有限。我们在Prompt中嵌入业务术语表:
“注意:'履约时效'指订单从支付到签收的小时数;'库存水位'指当前可售库存/安全库存阈值;'客诉分级'中P0=24小时内必须响应,P1=72小时内响应。”
这样模型在生成Thought时,会自然引用这些术语,减少歧义。比如用户问“订单12345的履约时效是否达标”,Thought会写“需查询订单12345的支付时间与签收时间,计算差值并与SLA标准(<24h)比对”。
要素三:强制思维链长度控制。我们发现Thought过长(>150字)会导致模型注意力分散。于是加入硬约束:
“Thought必须控制在80-120字之间。若事实过多,请优先选择最关键的一个。”
这迫使模型聚焦核心矛盾。测试显示,符合字数约束的Thought,后续Action准确率提升27%。
要素四:提供负向示例。在Few-shot示例中,我们特意加入一个失败案例:
用户问:“帮我订一张北京到上海的高铁票。”
❌ 错误Thought:“我要订票,所以调用booking_api。”(未说明需获取车次、日期、座位类型)
✅ 正确Thought:“用户需从北京到上海的高铁票,需获取:1) 出发日期;2) 偏好车次(G/D字头);3) 座位类型(一等/二等)。因此调用booking_api查询余票。”
这种对比教学,比单纯说教有效得多。
提示:不要迷信“加大模型尺寸能解决Thought质量”。我们用GPT-4和Claude-3对比测试,发现Prompt设计对Thought质量的影响权重占68%,模型本身只占32%。花三天优化Prompt,比换模型收益更大。
3.3 输出解析器(OutputParser)的健壮性设计
ReAct的成败,一半在Prompt,一半在OutputParser。官方ReActOutputParser过于理想化,我们重写了三个关键层:
第一层:格式预检。在解析前,先用正则快速扫描输出文本:
def pre_check(text: str) -> bool: # 必须包含Thought/Action/Action Input/Observation四个关键词 required_keywords = ["Thought:", "Action:", "Action Input:", "Observation:"] return all(kw in text for kw in required_keywords) # 若缺失,立即返回结构化错误 if not pre_check(llm_output): return AgentFinish( return_values={"output": "Invalid ReAct format. Missing required sections."}, log="Format error: missing Thought/Action/Action Input/Observation" )第二层:字段精确定界。官方解析器用text.split("Action:")粗暴分割,易被模型生成的干扰文本破坏。我们改用状态机:
def parse_react_output(text: str) -> dict: state = "thought" result = {"Thought": "", "Action": "", "Action Input": "", "Observation": ""} lines = text.split("\n") for line in lines: if line.strip().startswith("Thought:"): state = "thought" result["Thought"] = line.replace("Thought:", "").strip() elif line.strip().startswith("Action:"): state = "action" result["Action"] = line.replace("Action:", "").strip() elif line.strip().startswith("Action Input:"): state = "action_input" result["Action Input"] = line.replace("Action Input:", "").strip() elif line.strip().startswith("Observation:"): state = "observation" result["Observation"] = line.replace("Observation:", "").strip() else: # 追加到当前字段(处理多行内容) if state == "thought": result["Thought"] += "\n" + line.strip() # ... 其他state同理 return result第三层:语义校验。解析后检查逻辑一致性:
def semantic_validate(parsed: dict) -> bool: # Action必须是已注册工具名 if parsed["Action"] not in TOOL_REGISTRY: return False # Action Input必须是合法JSON(若工具要求JSON参数) if TOOL_REGISTRY[parsed["Action"]].requires_json: try: json.loads(parsed["Action Input"]) except: return False # Observation不能为空(除非工具明确允许) if not parsed["Observation"].strip() and not TOOL_REGISTRY[parsed["Action"]].allows_empty_obs: return False return True校验失败时,不直接报错,而是生成AgentStep让模型重试:“Observation为空,但weather_api必须返回数据,请重试”。
实操心得:我们曾因忽略“多行内容追加”逻辑,导致模型在Thought中换行后,解析器只取了第一行。后来在状态机里加入
else分支处理续行,问题解决。这种细节,官方文档从不提,但线上故障往往就卡在这里。
4. 实操全流程与关键环节实现:从本地验证到生产部署
4.1 本地最小可行验证(MVP):5分钟跑通ReAct流程
别一上来就搞复杂工具链。我们用最简方案验证ReAct核心逻辑是否work:
步骤1:定义一个哑工具(Dummy Tool)
from langchain.tools import BaseTool class DummySearchTool(BaseTool): name = "dummy_search" description = "A fake search tool that returns fixed results. Use for testing." def _run(self, query: str) -> str: # 模拟不同查询返回不同结果 if "langchain" in query.lower(): return "LangChain is a framework for developing applications powered by LLMs." elif "react" in query.lower(): return "ReAct is a prompting strategy that combines reasoning and acting." else: return f"Search result for '{query}' (dummy)." async def _arun(self, query: str) -> str: return self._run(query)步骤2:构建ReAct Agent Executor
from langchain.agents import AgentExecutor, create_react_agent from langchain import hub from langchain_openai import ChatOpenAI # 加载ReAct提示模板(我们修改过的版本) prompt = hub.pull("hwchase17/react-chat") llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) tools = [DummySearchTool()] # 创建Agent(注意:这里用create_react_agent,不是create_zero_shot_agent) agent = create_react_agent(llm, tools, prompt) agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True) # 测试 result = agent_executor.invoke({"input": "What is LangChain?"}) print(result["output"]) # 输出应为:"LangChain is a framework for developing applications powered by LLMs."关键验证点:
- 查看
verbose=True输出,确认是否出现Thought:、Action:、Action Input:、Observation:四段式日志; - 修改
DummySearchTool,让它在_run中故意抛异常,观察handle_parsing_errors=True是否生效(应返回友好错误而非崩溃); - 手动构造一个格式错误的LLM输出(如删掉
Observation:),确认预检逻辑是否拦截。
这5分钟验证,能排除80%的环境配置问题。很多团队卡在“跑不通”,其实是连基础流程都没走通。
4.2 生产级工具链集成:天气、搜索、计算器三工具协同实战
真实场景需要多工具协同。我们以“规划周末上海行程”为例,演示三工具如何联动:
用户输入:“这个周末去上海玩,天气怎么样?附近有什么推荐景点?预算2000元够吗?”
预期ReAct流程:
- Thought:需获取上海周末天气、上海景点列表、2000元在上海的消费能力评估 → Action: weather_api → Action Input: {"city": "Shanghai", "date": "this weekend"}
- Observation:{"temp_min": 18, "temp_max": 25, "rain_prob": 30%, "condition": "Partly cloudy"}
- Thought:天气适宜,需获取景点信息 → Action: search_api → Action Input: {"query": "top attractions in Shanghai", "limit": 5}
- Observation:[{"name": "The Bund", "entry_fee": 0}, {"name": "Yu Garden", "entry_fee": 20}, ...]
- Thought:需评估2000元能否覆盖景点门票+餐饮 → Action: calculator_api → Action Input: "2000 - (20 + 120 + 80) * 2" (假设3个景点,2天餐饮)
工具集成要点:
- 天气工具:我们用
requests调用免费Open-Meteo API,返回后经ObservationFormatter压缩为:“Shanghai this weekend: 18-25°C, 30% rain chance, partly cloudy.” - 搜索工具:接入Serper API,返回JSON后提取
organic字段的title和snippet,格式化为:“1. The Bund: Iconic waterfront area. 2. Yu Garden: Classical Chinese garden, entry fee ¥20.” - 计算器工具:用
numexpr安全计算,支持四则运算和括号,拒绝__import__等危险操作。
关键代码片段(工具注册与Executor初始化):
from langchain.agents import AgentExecutor from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder # 自定义Prompt(含领域知识) prompt = ChatPromptTemplate.from_messages([ ("system", "You are a travel assistant. Use tools to answer questions about destinations. " "Remember: Thought must explain WHY you need the info, Action must be exact tool name, " "Action Input must be valid JSON, Observation must be concise."), MessagesPlaceholder(variable_name="chat_history"), ("human", "{input}"), MessagesPlaceholder(variable_name="agent_scratchpad"), ]) # 创建Agent(非create_react_agent,而是手动组装) llm_with_tools = llm.bind_tools(tools) agent = ( { "input": lambda x: x["input"], "agent_scratchpad": lambda x: format_to_openai_tool_messages(x["intermediate_steps"]), "chat_history": lambda x: x["chat_history"] } | prompt | llm_with_tools | ReActOutputParser() # 我们自研的解析器 ) agent_executor = AgentExecutor( agent=agent, tools=tools, verbose=True, max_iterations=15, early_stopping_method="generate", # 卡住时生成结束 handle_parsing_errors="Check your output and make sure it contains a thought, action, action input, and observation." )性能实测数据(GPT-3.5-turbo,AWS c5.2xlarge):
- 单工具调用(如只查天气):平均1.2秒(含网络延迟)
- 三工具串联(上述行程规划):平均2.8秒,95分位3.4秒
- 对比默认ZeroShotAgent:同样任务平均4.7秒,95分位6.2秒
4.3 生产环境部署:监控、降级与灰度发布策略
上线不是终点,而是运维的开始。我们为ReAct Agent设计了三层防护:
第一层:实时监控看板
用Prometheus+Grafana监控核心指标:
react_agent_request_total{status="success"}/status="error":成功率react_agent_step_count{step="thought"}/step="action":各环节耗时分布react_agent_tool_call_total{tool="weather_api"}:各工具调用量及错误率
特别关注react_agent_parsing_error_total——这是Prompt或OutputParser缺陷的直接信号。当该指标突增,立即触发告警,暂停灰度流量。
第二层:动态降级开关
在配置中心(Apollo)设置开关:
react_agent.enabled=true:全局启用react_agent.tool.weather_api.fallback="historical":天气工具降级为返回历史均值react_agent.max_iterations=10:紧急情况下缩短迭代次数
降级逻辑在ToolExecutor中实现:
def execute_tool(tool_name: str, input_dict: dict) -> str: if is_fallback_enabled(tool_name): return get_fallback_result(tool_name, input_dict) # 否则正常调用 return tool.run(input_dict)第三层:灰度发布流程
- Step1:1%流量走ReAct Agent,99%走旧ZeroShotAgent,对比成功率与耗时;
- Step2:当ReAct成功率>99.5%且P95耗时<3.5秒,开放至10%;
- Step3:插入A/B测试分流,同一用户连续5次请求必须走同一路径,避免体验割裂;
- Step4:全量前,用历史请求回放(replay)验证:取1000条线上日志,批量跑ReAct,确认无新增错误类型。
实操心得:我们第一次灰度时,发现ReAct在处理“否定句”时表现差(如“不要推荐收费景点”),Thought常忽略“不要”二字。原因是Prompt中没强调否定词识别。我们立刻在Prompt中加入:“Thought必须显式分析用户语句中的否定词(not, no, don't, 无需, 不要),并在Action中体现过滤逻辑。” 两小时后上线热修复,问题解决。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 典型问题速查表
| 问题现象 | 根本原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 模型反复调用同一工具,不收敛 | Thought未体现“已获取所需信息”,或Observation未包含足够决策依据 | 1. 查看Thought字段是否提及“已完成”;2. 检查Observation是否被截断(如JSON过长);3. 确认工具返回是否含关键字段 | 在Prompt中强化:“若Observation已包含答案,直接输出Final Answer,禁止再次调用工具”;在ObservationFormatter中强制截断至500字符 |
| Action Input解析失败,报JSONDecodeError | 模型生成的Action Input含中文引号、多余空格、或非法字符 | 1. 日志中提取原始Action Input;2. 用json.loads()手动测试;3. 检查是否含\n或“(中文引号) | 在OutputParser中添加清洗:input_clean = re.sub(r'[^\x00-\x7F]+', '', input_raw).replace('“','"').replace('”','"') |
| Observation为空,但工具实际返回了数据 | 工具执行异常被捕获,但except块未返回有意义的Observation | 1. 查看工具代码的except分支;2. 检查是否return ""或return None | 强制所有except分支返回f"Tool error: {str(e)}",绝不返回空字符串 |
| 多轮对话中,模型“忘记”之前结论 | Memory未正确注入,或Observation未包含关键结论 | 1. 检查agent_scratchpad是否包含上一轮Observation;2. 确认ObservationFormatter是否丢失了结论性语句 | 在ObservationFormatter中添加摘要逻辑:“若Observation含数值结论(如‘温度22°C’),强制前置” |
| ReAct Agent比ZeroShot更慢 | 工具调用未并行,或Observation过大拖慢LLM理解 | 1. 查看日志确认工具是否串行调用;2. 统计Observation平均长度 | 对独立工具(如天气、搜索)启用asyncio.gather并行调用;Observation长度限制为300字符 |
5.2 独家避坑技巧:来自血泪教训的5条军规
军规一:永远不要信任模型生成的Action Input
我们曾因模型在Action Input中生成{"city": "Shanghai, China"}(带逗号),导致天气API解析失败。后来在ToolExecutor中加入强校验:
def validate_city(city: str) -> str: # 移除所有标点,只留字母数字空格 clean = re.sub(r'[^a-zA-Z0-9\s]', '', city) # 取第一个单词作为城市名("Shanghai China" → "Shanghai") return clean.split()[0] if clean.split() else "Shanghai"所有工具参数都经过此类清洗,再传给真实API。
军规二:Observation必须“人话”,不能是机器话
早期我们直接把API返回的JSON塞进Observation,模型常被"code":200,"message":"success"干扰。现在强制转换:
# 原始 {"code":200,"data":{"temp":22,"humidity":65}} # 转换后 "Current temperature is 22°C, humidity is 65%."规则很简单:Observation只能是主谓宾完整句子,不含任何键名、状态码、技术字段。
军规三:为每个工具设定“思考冷却期”
模型容易陷入“查A→查B→查A→查B”循环。我们在Thought校验中加入:
# 若上一轮Action是weather_api,本轮Thought中禁止出现"weather"、"temperature"等词 if last_action == "weather_api" and any(word in thought.lower() for word in ["weather", "temp", "rain"]): return "Avoid repeating weather queries. Focus on next required fact."这招让循环调用率下降92%。
军规四:Final Answer必须可验证
我们要求所有Final Answer必须能被工具反向验证。例如:
- 用户问“上海明天最热多少度?”,Answer必须是“25°C”(纯数字);
- 若Answer是“大概25度左右”,则视为不合格,强制重试。 这样确保答案可被自动化测试,避免模糊表述。
军规五:日志必须记录“决策树”
我们不只记Thought,还记录模型的“备选Thought”:
# 在LLM调用前,注入: "Consider these options: 1) Call weather_api to get temp; 2) Call search_api to get travel tips; 3) Call calculator_api to check budget. Choose ONE."然后在日志中记录模型实际选择的序号。这让我们能分析:模型为何选A不选B?是Prompt引导不足,还是工具描述有歧义?
最后分享一个小技巧:当你发现ReAct效果不稳定,先别急着调模型参数。打开日志,随机抽10条失败case,只看Thought字段——90%的问题根源都在这里。Thought清晰,Action自然准确;Thought混乱,后面全是徒劳。把精力花在打磨Thought的引导语上,比换十个模型都管用。
