ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

经典 ReAct 的“文本标签之痛

经典 ReAct 的“文本标签之痛 系列第二篇。上一篇建立了 ReAct 的直觉,这一篇要问一个尖锐的问题:ReAct 原论文让模型吐Thought:/Action:/Observation:文本标签,而 LoopAgent 一个标签都没用——为什么?⭐经典 ReAct 长什么样2022 年那篇 ReAct 论文(Yao et al.)里,Agent 的工作方式是让模型在一整段文本里,按固定格式吐出交替的推理和动作:Thought: 我需要查一下登录超时相关的代码 Action: search[login timeout] Observation: 找到 3 处:session.ts、loginService.ts、httpClient.ts Thought: httpClient 的超时只有 3 秒,可疑,读一下细节 Action: read[loginService.ts] Observation: ECONNABORTED 被翻译成了登录超时 Thought: 根因清楚了 Final Answer: 根因是 httpClient 超时太短……然后程序这边写一个解析器,用正则去这段文本里抠东西:找到Action:那一行,把search[...]里的动作名和参数切出来,去执行;执行完把结果拼成Observation: ...再接到文本后面,重新喂给模型。整个循环,靠的是模型老老实实按格式写字。这就是问题的起点。痛点一:模型不一定按格式写字模型是概率生成的,它没有义务严格遵守你在 prompt 里定的格式。真实世界里会发生:模型把Action:写成中文动作:,正则匹配不到;参数里带了引号或换行 ——Action: edit[filea.ts, contentline1\nline2]—— 括号/引号配对被打乱,切出来的参数是错的;模型一时兴起把Thought和Action顺序写反,或一段里塞了两个Action:;模型忘了写Final Answer:,程序永远等不到结束信号,循环停不下来。任何一种,解析器就地失败,整局崩溃。而且这种失败很难向模型说清楚,因为你连它到底哪里写错了都难结构化地定位。✶ Insight ─────────────────────────────────────这不是模型不够强的问题,再强的模型也是概率生成,总有小概率跑偏。问题在于架构把可靠性押在了模型的文本纪律上——而这是整个系统里最不可控的一环。工程上,把关键正确性依赖压在一个概率组件的自觉性上,是危险的设计。─────────────────────────────────────────────────解法:用模型的原生工具调用替代文本标签2023 年之后,OpenAI 等把工具调用(function calling / tool calling)做进了模型协议本身。模型不再需要在正文里手写Action: search[...],而是通过一个结构化的协议字段告诉你我要调用哪个函数、参数是什么。LoopAgent 走的就是这条路。看它接收模型输出的地方(openAiReactModelTurn.ts:52-58):}elseif(event.typetoolCallDelta){constpendingpendingCalls.get(event.index)??{name:,arguments:};pendingCalls.set(event.index,{id:event.id??pending.id,name:pending.name(event.name??),arguments:pending.arguments(event.argumentsDelta??),});}这里消费的toolCallDelta是协议层的结构化事件,动作是{ id, name, arguments }这样的对象,arguments是 JSON 字符串。程序不再去正文里正则抠Action:,而是直接读协议字段。Thought 和 Observation 也各自有了专门的通道,不再混在正文里:ReAct 概念经典文本范式LoopAgent(函数调用式)Thought正文里的Thought:行模型的reasoning通道(reactTypes.ts:14)Action正文里的Action:,正则解析原生tool_calls(结构化 JSON)Observation拼进正文的Observation:文本独立的role:tool消息优势一:解析失败从全局崩溃降级为局部可恢复即使模型把参数写成了非法 JSON,函数调用式也不会崩。看解析处(openAiReactModelTurn.ts:117-123):letinput:unknown;letparseError:string|undefined;try{inputJSON.parse(pending.arguments);}catch{parseErrorInvalid JSON arguments for tool${pending.name};// 只记一个错,不 throw}这个parseError会被带到 runner,变成一条喂回模型的错误消息(reactAgentRunner.ts:208-210):if(toolRequest.parseError){return{content:Tool error:${toolRequest.parseError},succeeded:false,/* ... */};}模型下一轮就能看到哦我上次参数写错了,自我纠正。经典文本范式里,一次解析失败往往是不可恢复的——你甚至难以结构化地告诉模型错在哪。优势二:Observation 和 Action 强绑定,归因清晰经典范式把工具结果拼成Observation: ...的普通文本,和模型自己的Thought:混在同一段里,模型分不清哪句是我推理的、哪句是环境返回的。上下文一长,幻觉风险陡增。LoopAgent 用独立的role:tool消息回填,还带requestId(reactAgentRunner.ts:261-266):messages.push({role:tool,requestId:request.id,// ← 精确挂回发起它的那次 actionname:request.name,content,});每条 observation 通过requestId精确对应到发起它的那次 action。模型天然知道这是 call_a1 那次搜索的结果,角色边界由消息结构划定,而不是靠文本约定。优势三:停机判定从文本约定变成协议信号“什么时候算说完了”——两种范式的依据完全不同:经典:等模型吐Final Answer:这行文本,正则命中才停。模型忘写标签 停不下来。LoopAgent:看这一轮模型有没有发 tool_calls。有就是继续行动,没有且有正文就是最终答案(openAiReactModelTurn.ts:76-81):if(content.length0){if(toolChoicerequired||typeoftoolChoiceobject){thrownewError(Model did not call a required tool);}return{kind:final,content,...(reasoning?{reasoning}:{})};// ← 没调工具 有正文 收尾}判定依据从模型的文本自觉变成了finishReason这个协议字段,鲁棒性质变。✶ Insight ─────────────────────────────────────在可靠的停机信号之上,LoopAgent 还能做经典范式难做的倒计时收尾:临近步数上限时,把toolChoice强制设成none,从协议层禁掉工具逼模型给答案(reactAgentRunner.ts:132)。文本范式里你只能在 prompt 里求它收尾,约束力弱得多。这个机制第 05 篇会细讲。─────────────────────────────────────────────────但要诚实:函数调用式也有代价天下没有免费的优势。函数调用式换来一个硬约束:它绑定了支持工具调用协议的模型。文件名openAiReactModelTurn.ts就暴露了这层依赖——它消费的是 OpenAI 兼容的流式工具调用协议(DeepSeek 等也兼容)。面对只能吐纯文本的本地小模型、老式补全接口,经典文本标签 ReAct 反而是唯一可行的路——它只要求模型会输出文本。好在 LoopAgent 把模型这一回合怎么进行抽象成了一个接口ReactModelTurn(reactTypes.ts:75):exporttypeReactModelTurn(input:{messages:ReactAgentMessage[];signal:AbortSignal;toolChoice?:ModelToolChoice;})PromiseReactModelTurnResult;openAiReactModelTurn只是它的一个实现。将来要接一个只吃文本的模型,再写一个文本解析版的ReactModelTurn实现即可,runner 主循环一行都不用动。这是把协议依赖隔离在边界层的好设计。小结维度经典文本标签函数调用式(LoopAgent)动作格式模型自觉写Action:,正则解析原生tool_calls,协议保证解析失败常导致整局崩溃降级为可恢复的错误消息Observation 归因混在正文,边界模糊role:toolrequestId强绑定停机判定靠Final Answer:文本靠finishReason协议信号模型要求只要会吐文本(更通用)需支持工具调用协议一句话:函数调用式在可靠性、归因、停机三个维度全面胜出,代价是绑定工具调用协议;经典范式只在模型不支持工具调用时才更优。下一篇,我们把 LoopAgent 的 ReAct 主循环逐帧慢放,从用户提问到最终答案,看每一条消息怎么在 system/user/assistant/tool 之间流动。本文源码来自开源项目LoopAgent⭐ → https://github.com/oi12344/loopagent-vscode
返回列表