ReAct智能体原理与实战:让AI边思考边行动 1. 项目概述当“想清楚再动手”不再适用时我们该怎么让AI真正动起来你有没有遇到过这种场景客户甩来一句“帮我查下特斯拉现在的股价再结合最近三个月的走势判断现在是不是买入的好时机”——乍一看是个简单需求但真要落地你会发现它根本没法被拆成“第一步查股价、第二步拉K线图、第三步写分析报告”这样的固定流程。为什么因为第二步拉出来的K线图如果显示剧烈震荡你可能得立刻回头去查美联储最新会议纪要如果发现是某条突发新闻引发的跳空缺口你又得马上搜索那条新闻的全文和市场反应。整个过程像在迷雾中探路每一步都得踩实了才知道下一步该往哪走。这正是ReAct模式要解决的核心问题当任务无法被预先分解当信息必须边走边挖当“思考”和“行动”必须长在同一根神经上时我们不能再靠一张静态流程图指挥AI而要给它装上一套实时反馈的闭环控制系统。这不是简单的“调用工具”而是让模型在每一次推理后都能基于刚刚获得的观测结果动态决定是继续深挖、切换方向还是直接收工。它把传统AI系统里泾渭分明的“大脑”推理和“手脚”工具调用彻底缝合在一起形成一个“想一点、做一点、看一眼、再想一点”的有机循环。如果你正在设计需要处理模糊需求、实时数据或复杂调研的智能体或者你总在纠结“为什么我的Agent明明有搜索能力却总在第一步就卡死”那么ReAct就是那个绕不开的底层范式。它不改变大模型本身的能力边界却从根本上重塑了能力释放的方式——从被动执行预设剧本转向主动驾驭不确定性的动态决策。2. 核心设计逻辑为什么必须把“思考”和“行动”锁死在同一个循环里2.1 传统模式的硬伤计划先行的思维惯性如何成为枷锁我们先拆解一下为什么之前那些看似精巧的模式在面对“特斯拉股价分析”这类问题时会集体失灵。以Orchestration编排为例它的典型工作流是用户提问 → 规划器Planner一口气把任务拆成“查股价→拉3个月K线→计算均线→对比行业指数→生成结论”5个子任务 → 构建执行图 → 各节点按序运行。这个模式在处理“北京到上海高铁时刻表”这种结构化查询时稳如老狗但一旦遇到真实世界的问题它的致命缺陷就暴露无遗。核心问题在于“规划前置”与“信息后置”的根本矛盾。规划器在启动时对股价数据一无所知它只能凭经验猜“大概率需要5步”。可现实是第一步查出的股价如果是历史最高位模型可能需要立刻触发“查公司财报”和“查机构研报”两个新分支如果发现是暴跌又得紧急加入“查突发新闻”和“查同板块股票表现”。这些分支在规划阶段根本无法预见。我试过强行给Orchestration加“条件分支”结果代码复杂度指数级上升维护成本高到无法接受更别说动态调整了。这就像让一个没看过地图的司机先在纸上画好100条备选路线再上路——既浪费精力又大概率白画。2.2 Reflexion的局限把“研究”当成独立工序割裂了认知的连续性Reflexion模式试图用“先写草稿→再批判→最后补研究”的三段式来解决这个问题。听起来很合理先快速输出一个粗糙答案比如“特斯拉股价目前较高建议观望”然后批判环节指出“缺乏数据支撑”于是触发专门的研究步骤去查股价和K线。但问题在于这个“研究”是孤立发生的它和最初的草稿、批判结论之间没有活的上下文连接。研究模块拿到的指令只是“查股价”它不会知道批判者为什么觉得“缺乏数据支撑”——是因为价格异常还是因为趋势不明研究结果比如返回一个数字“245.67美元”被生硬地塞回修订环节模型得重新理解这个数字在原始语境中的意义。这中间存在两次信息损耗第一次是批判者无法精准描述研究需求第二次是研究结果无法自动锚定到推理链条的正确位置。我在实际调试中发现Reflexion经常陷入“查了A数据但真正需要的是B数据”的死循环。比如批判说“缺少趋势分析”研究模块就去拉了3个月K线图但模型看到K线图后才发现真正缺失的是“过去一周的成交量变化”而这个新需求在最初的批判里根本没被识别出来。Reflexion的“研究”像一个外包团队沟通成本太高。2.3 ReAct的破局点用状态累积构建“活的推理记忆”ReAct的精妙之处就在于它彻底废除了“规划”和“研究”这两个独立阶段把所有动作压缩进一个原子化的循环Thought → Action → Observation → Thought。关键在于那个状态State的持续累积机制。我们来看一个真实模拟的特斯拉分析片段Thought 1“用户问特斯拉股价和买入时机。首先需要获取当前实时股价。”Action 1调用get_stock_price(TSLA)Observation 1“245.67美元2024-03-04 14:22:18 UTC”Thought 2“当前价245.67需结合趋势。下一步应获取近3个月收盘价序列用于计算移动平均线。”Action 2调用get_stock_history(TSLA, period3m)Observation 2返回包含126个日期-价格对的JSON数组Thought 3“数据显示过去3个月股价从198.22升至245.67涨幅约24%。但需确认是否为持续上涨检查周线级别波动性…”看到区别了吗每一次Thought都不是从零开始而是基于前面所有Action和Observation构成的完整上下文。Observation 1的数字直接催生了Thought 2的具体需求Observation 2的数据结构又自然引导Thought 3聚焦于波动性分析。这个过程没有“草稿”需要被推翻没有“研究”需要被整合状态像滚雪球一样越积越厚推理链条天然连贯。我把它比作一个经验丰富的分析师在白板上边写边想他不会先写满整块板再擦掉重来而是每写下一个数据就立刻在旁边标注一个疑问或推论新写的永远叠加在旧内容之上。这种“活的记忆”让模型能真正实现“边走边看边看边想”而不是在多个静态文档间疲于奔命地跳转。2.4 控制结构的本质从“流程图”到“神经反射弧”如果把Orchestration比作工厂流水线每个工位固定做一件事Reflexion比作论文写作初稿→审稿→修改那么ReAct就是人体的膝跳反射——一个最简化的生物反馈环。敲击膝盖肌腱Action→ 感觉神经传入信号Observation→ 脊髓中枢即时处理Thought→ 运动神经发出指令Action→ 小腿弹起。整个过程不经过大脑皮层没有“计划”只有对刺激的即时响应。ReAct的控制图StateGraph正是模拟了这个生理结构agent节点脊髓中枢接收当前状态决定是否调用工具tools节点效应器执行并返回结果结果自动注入状态触发agent下一次处理。这个环路的威力不在于单次Thought有多深而在于它能无限次、低成本地完成“感知-决策-执行”的微循环。它把复杂的宏观任务分解为一系列微观的、可验证的原子操作。每一个Action都是一个可测试的假设“查股价能回答问题吗”每一次Observation都是对假设的验证“查到了但还不够”而Thought就是根据验证结果生成新假设的过程。这种设计让系统具备了真正的适应性——它不需要预知终点只要保证每一步都朝着目标微调最终必然抵达。这才是应对不确定性世界的底层智慧。3. 实操细节解析从概念到可运行代码的关键落点3.1 状态State设计消息序列为何是唯一合理的载体在ReAct实现中AgentState的定义看似简单却暗藏玄机。原文给出的class AgentState(TypedDict): messages: Annotated[Sequence[BaseMessage], add_messages]绝非随意选择。我曾尝试过其他状态设计比如用字典分别存current_question、collected_data、analysis_notes结果在实践中迅速暴露出严重问题。消息序列Message Sequence是唯一能无损承载ReAct动态特性的状态形式。原因有三第一它天然保留了时间顺序。Thought 1必须在Action 1之前Observation 1必须在Thought 2之前这种严格的时序依赖用扁平字典根本无法表达。第二它完美支持增量更新。每次add_messages操作只是把新消息追加到列表末尾旧消息毫发无损。而如果用字典每次更新collected_data都可能覆盖之前的分析笔记导致上下文断裂。第三它与大模型的输入格式完全对齐。所有主流LLM APIOpenAI, Anthropic, Ollama都要求将对话历史作为messages列表传入其中每条消息明确标记roleuser/system/assistant/tool。这意味着你的状态设计与模型接口零摩擦无需任何转换开销。提示不要试图在状态里存储“结构化数据”。比如有人想把get_stock_history返回的JSON直接解析成Python字典存进state这是危险的。因为模型看到的永远是原始文本Observation它需要自己去解析、理解、关联。如果你提前解析反而破坏了模型的推理自主性且一旦API返回格式微调你的解析逻辑就会崩溃。让Observation保持“原汁原味”才是对模型最大的尊重。3.2 模型节点call_model提示词工程的生死线call_model函数是ReAct的大脑它的提示词Prompt质量直接决定整个系统的智商上限。一个常见误区是认为“只要模型够强提示词随便写”。我踩过的最大坑就是在初期用了一个过于宽泛的提示“你是一个AI助手请回答用户问题。你可以使用以下工具[tool list]。” 结果模型要么疯狂调用工具却不思考要么死活不肯调用工具一直在自说自话。ReAct提示词的核心是强制模型显式输出“Thought-Action-Observation”三段式结构并用特殊分隔符锁定其行为边界。我现在稳定使用的模板如下已脱敏You are a precise, step-by-step reasoning agent. Your task is to answer the users question by interleaving thought and action. Follow this strict format: |thought|Your internal reasoning here. Explain why you need the next action, or why you can now answer. |action|tool_name(tool_args) |observation|The result of the tool will be inserted here automatically. Current conversation history: {scratch_pad} Important rules: - NEVER generate |observation| yourself. It will be provided by the system. - If you have enough information to answer the question definitively, output ONLY |thought| followed by your final answer. DO NOT include |action|. - If you need more information, output |thought| explaining why, then |action| with the exact tool call. - Use only tools from the list below. Do not invent new tools. - Be concise in your thoughts; focus on the logical necessity of the next step.这个模板的每个细节都有深意|thought|等分隔符让模型明确知道各部分的语义角色避免混淆强调“NEVER generate |observation|”防止模型幻觉规则中反复强调“ONLY”和“DO NOT”用强硬语气约束模型行为。最关键的是它把“思考”和“行动”的因果关系具象化——Thought必须解释“为什么需要Action”这强迫模型进行真正的推理而非机械填空。实测下来用这个提示词GPT-4-turbo的工具调用准确率从不足40%提升到92%以上。3.3 终止条件should_continue如何防止AI陷入无限循环should_continue函数是ReAct系统的安全阀它的逻辑错误会导致灾难性后果。原文的if not last_message.tool_calls: return end看似简洁但在真实场景中远远不够。我遇到过最棘手的情况是模型在Thought中写道“我需要更多信息”却忘记生成tool_calls字段导致系统误判为“可以结束”返回一个半成品答案。更隐蔽的陷阱是“工具调用失败”——比如网络超时工具节点返回空结果或错误信息此时last_message.tool_calls依然存在但should_continue会错误地进入continue分支形成死循环。一个健壮的终止条件必须是多维度的熔断机制。我现在的实现包含三层校验def should_continue(state: AgentState) - str: messages state[messages] if len(messages) 0: return end last_message messages[-1] # 第一层检查是否已生成最终答案无tool_calls且内容完整 if not last_message.tool_calls: # 检查是否为有效回答长度、关键词、无占位符 content last_message.content.strip() if (len(content) 20 and answer in content.lower() and not enough information not in content.lower()): return end # 第二层检查工具调用是否成功观察最新Observation # 遍历消息找最近的Observation latest_observation None for msg in reversed(messages): if hasattr(msg, name) and msg.name tool_response: latest_observation msg.content break if latest_observation: # 检查Observation是否为空、错误或超时 if (error in latest_observation.lower() or timeout in latest_observation.lower() or len(latest_observation.strip()) 5): return end # 失败则终止避免重试 # 第三层全局迭代次数熔断防死循环 if len(messages) 15: # 最多15轮交互 return end return continue这个版本增加了对答案质量的语义判断、对工具结果的健康检查、以及硬性的迭代次数限制。它不再天真地相信tool_calls字段的存在而是综合评估整个对话状态。在生产环境中这三层熔断让我避免了99%以上的无限循环事故。3.4 工具节点tool_node让工具调用成为可预测的确定性事件工具节点tool_node常被低估但它其实是ReAct稳定性的基石。很多新手直接把requests.get()塞进去结果系统在高并发下频繁超时整个ReAct循环卡死。工具节点的设计哲学是它必须是确定性的、可监控的、有兜底的。我的标准实践包括统一超时与重试所有工具调用必须包裹在tenacity库中设置stopstop_after_attempt(3)和waitwait_exponential(multiplier1, min1, max10)。这确保单次工具失败不会拖垮整个流程且重试间隔指数增长避免雪崩。标准化错误处理无论底层API如何抛错tool_node必须捕获并返回结构化错误信息。例如try: response requests.get(url, timeout10) response.raise_for_status() return {status: success, data: response.json()} except requests.exceptions.Timeout: return {status: error, message: timeout, detail: Tool request timed out after 10s} except Exception as e: return {status: error, message: unknown, detail: str(e)}结果缓存针对读操作对于get_stock_price这类高频、低变的读操作我在工具节点内加了一层LRU缓存lru_cache(maxsize128)缓存键为(tool_name, args_tuple)。这能将重复查询的延迟从300ms降到1ms极大提升用户体验。缓存失效策略很简单所有写操作如update_user_profile触发全量缓存清空。注意工具节点返回的内容必须严格匹配模型提示词中|observation|的预期格式。我通常会让工具节点返回一个JSON字符串再由tool_node外层包装成标准的ToolMessage对象。这样既保证了内部逻辑清晰又确保了外部接口的稳定性。4. 完整实操流程从零搭建一个可运行的ReAct股价分析Agent4.1 环境准备与依赖安装在开始编码前确保你的Python环境满足最低要求。我推荐使用Python 3.10因为它对类型提示TypedDict的支持更完善。核心依赖只有三个但版本选择至关重要# 创建干净的虚拟环境 python -m venv react_env source react_env/bin/activate # Linux/Mac # react_env\Scripts\activate # Windows # 安装核心依赖注意版本 pip install langgraph0.1.42 # LangGraph 0.1.x 是当前最稳定的ReAct实现框架 pip install langchain-core0.2.15 # 与langgraph 0.1.x 兼容的langchain版本 pip install openai1.35.14 # OpenAI SDK避免使用beta版以防API变更关键经验LangGraph在0.2.x版本中重构了StateGraph API与本文描述的add_conditional_edges等方法不兼容。我曾因升级到0.2.x导致整个ReAct循环无法编译调试了整整一天才定位到版本冲突。务必锁定langgraph0.1.42。同样langchain-core必须匹配高版本会引入不兼容的BaseMessage类。4.2 定义工具构建可信赖的“手脚”ReAct的“手脚”必须可靠。我们以股价查询为例实现一个生产级的get_stock_price工具。它不能只是一个简单的API调用而要包含数据验证和降级策略import json import time from typing import Dict, Any from langchain_core.tools import tool from tenacity import retry, stop_after_attempt, wait_exponential # 模拟一个可靠的金融数据API客户端实际项目中替换为Alpha Vantage/Yahoo Finance等 class FinancialAPIClient: def __init__(self): self.cache {} # 简单内存缓存生产环境建议用Redis retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min1, max10)) def get_stock_price(self, symbol: str) - Dict[str, Any]: # 模拟网络请求延迟 time.sleep(0.5) # 模拟真实API响应含错误场景 if symbol.upper() INVALID: raise Exception(Invalid symbol) # 返回结构化数据包含时间戳和来源便于模型验证 return { symbol: symbol.upper(), price: round(240.0 (time.time() % 10), 2), timestamp: time.strftime(%Y-%m-%d %H:%M:%S UTC, time.gmtime()), source: simulated_financial_api_v1 } # 创建全局客户端实例 finance_client FinancialAPIClient() tool def get_stock_price(symbol: str) - str: Get the current real-time price for a stock symbol. Returns a JSON string with keys: symbol, price, timestamp, source. Example: {symbol: TSLA, price: 245.67, timestamp: 2024-03-04 14:22:18 UTC, source: simulated_financial_api_v1} try: data finance_client.get_stock_price(symbol) return json.dumps(data, ensure_asciiFalse) except Exception as e: error_msg fError fetching price for {symbol}: {str(e)} return json.dumps({error: True, message: error_msg}, ensure_asciiFalse)这个工具的亮点在于1retry装饰器确保网络抖动不影响整体流程2返回JSON格式与模型提示词中|observation|的预期完美匹配3包含timestamp和source字段让模型能自行判断数据新鲜度和可信度。我特意在get_stock_price中加入了time.sleep(0.5)来模拟真实API延迟这在调试时非常有用——你能清晰看到ReAct循环的节奏感。4.3 构建ReAct核心循环StateGraph的精确布线现在我们将所有组件组装成一个可运行的StateGraph。这是最易出错的环节必须严格遵循LangGraph 0.1.x的语法from typing import TypedDict, Sequence, Annotated from langchain_core.messages import BaseMessage, ToolMessage from langgraph.graph import StateGraph, END from langgraph.prebuilt import ToolNode from langchain_openai import ChatOpenAI # 1. 定义状态必须使用TypedDict且字段名严格匹配 class AgentState(TypedDict): messages: Annotated[Sequence[BaseMessage], add_messages] # 2. 初始化大模型使用gpt-4-turbo平衡效果与成本 llm ChatOpenAI( modelgpt-4-turbo, temperature0.3, # 降低随机性让Thought更稳定 max_tokens1024, # production环境务必加上api_key参数 ) # 3. 构建模型节点call_model def call_model(state: AgentState) - dict: # 构造完整的提示词这里简化实际项目中应从文件加载 system_prompt You are a precise, step-by-step reasoning agent... [此处插入3.2节的完整提示词] # 将当前消息历史传入模型 response llm.invoke([ {role: system, content: system_prompt}, *state[messages] # 展开消息列表 ]) # 关键将模型响应包装为BaseMessage以便后续处理 return {messages: [response]} # 4. 构建工具节点复用LangGraph内置的ToolNode tools [get_stock_price] # 注册所有可用工具 tool_node ToolNode(tools) # 5. 定义终止条件should_continue def should_continue(state: AgentState) - str: # 此处插入4.3节的完整实现 ... # 6. 组装StateGraph核心 workflow StateGraph(AgentState) # 添加节点 workflow.add_node(agent, call_model) # 模型推理节点 workflow.add_node(tools, tool_node) # 工具执行节点 # 添加边tools执行完总是回到agent workflow.add_edge(tools, agent) # 添加条件边agent节点根据should_continue决定走向 workflow.add_conditional_edges( agent, should_continue, { continue: tools, # 需要工具去tools节点 end: END # 可以结束到END } ) # 设置入口点 workflow.set_entry_point(agent) # 编译图生成可执行对象 app workflow.compile()这段代码的每一行都经过千锤百炼。特别注意workflow.add_edge(tools, agent)这一行——它意味着工具执行完毕后无条件返回agent节点。这是ReAct循环的物理基础。而add_conditional_edges则负责在agent节点内部做决策决定是继续循环还是退出。这种“固定路径条件分支”的组合正是ReAct控制结构的精髓。4.4 执行与调试观察ReAct循环的每一次心跳现在让我们运行这个Agent并像医生观察心电图一样细致分析每一次循环# 构建初始输入 initial_input { messages: [ (user, Whats the current stock price of Tesla, and is it a good time to buy based on recent trends?) ] } # 执行streamTrue可实时观察每一步 for event in app.stream(initial_input, stream_modevalues): # 打印当前状态重点看messages列表的演变 print(f\n--- Step {len(event[messages])} ---) last_msg event[messages][-1] if hasattr(last_msg, tool_calls) and last_msg.tool_calls: print(f[ACTION] Calling tool: {last_msg.tool_calls[0].name}) print(f[ARGS] {last_msg.tool_calls[0].args}) elif hasattr(last_msg, content): print(f[THOUGHT/ANSWER] {last_msg.content[:100]}...) else: print(f[UNKNOWN] {type(last_msg)}) # 最终输出 final_state list(app.stream(initial_input))[-1] print(f\n FINAL ANSWER ) print(final_state[messages][-1].content)运行后你会看到类似这样的输出--- Step 1 --- [THOUGHT/ANSWER] |thought|The user asks for Teslas current stock price and whether its a good time to buy. First, I need to retrieve the current real-time price for TSLA... --- Step 2 --- [ACTION] Calling tool: get_stock_price [ARGS] {symbol: TSLA} --- Step 3 --- [THOUGHT/ANSWER] |thought|I have retrieved Teslas current price: $245.67. To assess if its a good time to buy, I need to examine recent performance trends. Next, I should fetch the stocks price history for the past 3 months... --- Step 4 --- [ACTION] Calling tool: get_stock_history [ARGS] {symbol: TSLA, period: 3m}这个实时日志就是你的调试圣经。每一步的Step N对应一次完整的Thought→Action→Observation循环。如果某一步卡住了你立刻就能定位是Thought没生成Action模型问题还是Action没触发Observation工具节点问题或是Observation没被正确注入状态StateGraph配置问题。我习惯在开发时把stream_modevalues换成stream_modedebug它会输出更详细的内部状态帮你揪出那些隐藏极深的bug。5. 常见问题与实战排障那些只有亲手踩过才知道的坑5.1 “模型死活不肯调用工具”提示词与工具注册的双重陷阱这是新手遇到的第一道墙。现象是模型在Thought中反复说“我需要查股价”但tool_calls字段始终为空。排查必须双线并行线索一检查工具注册。ToolNode(tools)中的tools列表必须是经过tool装饰器包装的函数对象。我曾犯过一个低级错误把get_stock_price函数名写成了字符串get_stock_price导致LangGraph根本找不到工具自然不会在模型输出中生成tool_calls。解决方案打印tools列表确认它是[function get_stock_price at 0x...]这样的函数对象而非字符串。线索二检查提示词中的工具列表。你的系统提示词里[tool list]部分必须与实际注册的工具完全一致。如果提示词写的是get_current_price(symbol)但注册的是get_stock_price(symbol)模型会困惑。最佳实践是在提示词中动态注入工具列表。例如在call_model函数中available_tools \n.join([f- {t.name}: {t.description} for t in tools]) system_prompt fYou can use the following tools:\n{available_tools}\n\n... rest of prompt ...这样保证了提示词和代码的绝对同步杜绝了人为疏忽。5.2 “工具调用成功但模型无视结果”Observation注入的隐形杀手现象是get_stock_price返回了{price: 245.67}但模型在下一步Thought中完全没提这个数字仿佛没看见。这几乎100%是Observation消息没有被正确添加到state[messages]中。LangGraph的ToolNode默认会将工具结果包装成ToolMessage但这个消息必须被add_messages机制捕获。关键检查点AgentState的messages字段的Annotated类型。必须是Annotated[Sequence[BaseMessage], add_messages]其中add_messages是字符串字面量不是变量。我曾因粗心写成Annotated[Sequence[BaseMessage], add_messages]少了引号导致add_messages未被激活Observation消息被静默丢弃。修复后你会在日志中看到ToolMessage对象出现在messages列表里模型才能“看见”它。5.3 “循环次数爆炸”如何优雅地为ReAct装上刹车片ReAct的灵活性是一把双刃剑。一个设计不良的提示词可能让模型在“查股价→查K线→查新闻→查财报→查研报→查同业→查宏观”中无限打转。除了4.3节提到的三层熔断我还增加了一个业务语义熔断def should_continue(state: AgentState) - str: # ... 前面的检查 ... # 新增业务逻辑熔断 # 如果已经调用了5个不同的工具且问题仍未解决强制终止 called_tools set() for msg in state[messages]: if hasattr(msg, tool_calls) and msg.tool_calls: called_tools.add(msg.tool_calls[0].name) if len(called_tools) 5: # 检查最后3次Thought是否都在重复同一类需求 recent_thoughts [] for msg in reversed(state[messages][-10:]): if hasattr(msg, content) and msg.content.startswith(|thought|): recent_thoughts.append(msg.content) if len(recent_thoughts) 3: break if (len(recent_thoughts) 3 and need more information in recent_thoughts[0].lower() and need more information in recent_thoughts[1].lower() and need more information in recent_thoughts[2].lower()): return end # 连续三次说需要更多信息说明思路已僵化 return continue这个逻辑模拟了人类分析师的直觉当你连续三次都说“还需要更多信息”往往意味着你该停下来换个思路而不是继续钻牛角尖。它让ReAct不仅有技术上的刹车还有业务上的智慧。5.4 “成本失控”Token爆炸的根源与抑制策略ReAct的token消耗是线性的但危害是指数级的。每一次循环messages列表都增长而模型每次都要重读全部历史。当循环到第10轮时输入token可能已达8000成本飙升。最有效的抑制策略是“状态裁剪”State Pruning。我的方案是在call_model函数中对传入模型的messages进行智能截断def call_model(state: AgentState) - dict: # 获取所有消息 all_messages state[messages] # 保留第一条user消息问题本质、最后一条assistant消息最新Thought、所有ToolMessage关键Observation # 裁剪中间的assistant消息旧Thought和system消息可复用 pruned_messages [all_messages[0]] # 保留初始问题 # 从后往前找保留最近的3个ToolMessage和它们之前的Thought tool_messages [] for msg in reversed(all_messages): if isinstance(msg, ToolMessage): tool_messages.append(msg) if len(tool_messages) 3: break # 将找到的ToolMessage及其前一个Thought加入pruned列表 for tool_msg in reversed(tool_messages): # 找到tool_msg之前的最后一个assistant消息 for prev_msg in reversed(all_messages[:all_messages.index(tool_msg)]): if hasattr(prev_msg, role) and prev_msg.role assistant: pruned_messages.append(prev_msg) break pruned_messages.append(tool_msg) # 确保最后一条是assistant消息最新的Thought if all_messages and hasattr(all_messages[-1], role) and all_messages[-1].role assistant: pruned_messages.append(all_messages[-1]) # 调用模型时只传pruned_messages response llm.invoke(pruned_messages) return {messages: [response]}这个裁剪算法保证了模型始终能看到“问题-最新思考-最近三次关键观测”这个最小黄金三角既不失上下文连贯性又将输入token稳定在2000以内。实测下来10轮循环的总token消耗从12000降至4500成本降低62%而准确率几乎无损。6. 场景适配与能力边界ReAct不是万能钥匙何时该用何时该换6.1 ReAct的黄金应用场景四类问题的精准识别ReAct并非银弹它的威力只在特定土壤中绽放。根据我两年来在金融、电商、医疗咨询等领域的落地经验以下四类问题ReAct是当之无愧的最佳选择多跳信息检索Multi-hop Retrieval问题答案分散在多个异构数据源中且后一跳的查询关键词必须从前一跳的结果中提取。例如“找出2023年销售额最高的国产新能源汽车品牌然后查该品牌在2024年