很多程序员第一次听到"大模型智能体"这个概念时,第一反应通常是:大模型不都已经能写代码、做推理了吗,为什么还要额外搭一套所谓的智能体架构?甚至有人把Agent等同于"提示词写得好一点"。我见过不少团队花了两周时间调prompt,最后发现模型还是在关键环节凭空编造数据。问题不在于模型的智商,而在于它被设计成"一次性回答",而不是"闭环完成任务"。这个大背景下,PEAS模型和ReAct架构就成了绕不开的两个理论锚点——前者是经典AI里定义"智能体是什么"的分析框架,后者是大模型时代让智能体真正"动起来"的运作方式。这篇文章想把这两块内容串起来讲清楚,适合正在做Agent应用开发、或者准备用大模型落地自动化流程的程序员参考。
1. 为什么大模型已经很会聊天,我们还要专门搭一套"智能体"框架
先说一个最容易被忽略的事实:大模型在单次对话里,本质上是无状态推理器。你丢给它一段文本,它吐出一段文本。它不知道这次对话要达到什么目标、不知道当前的进度走到哪一步、更无法主动去查询实时数据。它能写出一段看起来头头是道的旅游攻略,但如果真让它订机票,它连一个接口都调不动。这不是模型蠢,而是它的工作模式决定的——它只有"输入-输出"这根单行道。
智能体的核心价值,恰恰是给这个强大的"大脑"配上身体和反馈回路。一个完整的智能体,至少要能把任务拆分成多步、能调用外部工具获取真实信息、能根据执行结果修正下一步动作,最后再决定什么时候收尾。经典AI教材里对智能体的定义非常简单:通过传感器感知环境,通过执行器作用于环境。放到大模型语境下,传感器就是模型看到的用户消息、检索结果、工具返回值;执行器就是函数调用、代码执行、API请求。模型负责在这两者之间做决策,而这个"感知-决策-行动-再感知"的闭环,就是智能体的灵魂。
为什么程序员需要专门关注这件事?因为大模型的能力边界已经不再是"能不能生成文字",而是"能不能被可靠地放进自动化流程里"。如果你的需求只是写一段摘要、改一版文案,那单次对话足够。但任何涉及多步骤、外部系统、状态变更的需求——比如自动处理工单、定时巡检服务器、根据用户行为生成个性化报告——都需要一套明确的Agent框架来管理状态和动作。没有这个框架,模型就会在第二三步开始跑偏,或者原地打转,甚至自信地输出一个根本没执行过的结果。
我从2023年开始在项目里尝试这类结构,最早踩的坑就是把Agent等同于"多轮对话"。后来发现多轮对话只是形式,真正的关键在于决策循环的闭环设计:谁定义目标、谁评估进度、谁决定下一步、什么条件下终止。这些问题的答案,在PEAS模型里能找到一半,在ReAct架构里能找到另一半。
2. PEAS四要素放到大模型时代:性能度量、环境、执行器、传感器怎么重新翻译
PEAS是《人工智能》经典教材里提出的智能体设计分析框架,四个字母分别代表性能度量(Performance)、环境(Environment)、执行器(Actuators)、传感器(Sensors)。它本来的用途是帮你把一个智能体系统描述清楚:它要达成什么目标、活在什么世界里、有哪些手段改变世界、有哪些渠道感知世界。这套框架虽然诞生于大模型还没影的年代,但用在今天的LLM Agent设计上反而格外顺手,因为它的抽象层级足够高。
2.1 性能度量:如何定义"任务完成"
性能度量是PEAS里第一个要回答的问题:怎么判断这个Agent干得好不好?经典AI里这个指标往往很明确,比如棋类Agent的胜率、扫地机器人的覆盖面积。但到了LLM Agent,性能度量经常被模糊成一句"生成一个合理的结果",这会给后续开发埋雷。
具体做项目时,我习惯把性能度量拆成两层。最终结果层关心的是任务是否完成,比如客服Agent的"一次性解决率"、代码Agent的"测试用例通过率"。过程质量层关心的是执行的每一步是否合理,比如工具调用次数是否冗余、是否出现了无效的重复查询、有没有在关键步骤上跳过验证直接给结论。区分这两层非常重要,因为如果只度量最终结果,模型会被"诱导"着一步到位编答案;如果过程度量设得太细,模型又会变得畏手畏脚,每走一步都要反复确认,反而拖慢任务。
一个很典型的例子是我在某模拟项目里做的订单查询Agent。最初的性能度量只写了"正确回答用户订单状态",结果模型在遇到订单号不在数据库时,会直接虚构一个状态告诉用户。后来我把性能度量改成"用户问题必须匹配到数据库记录后才能回答,查不到时必须明确说明未找到",模型的行为立刻发生了变化——它会更倾向于调用查询工具而不是凭经验猜。这说明性能度量本身就是在向模型传达价值观,它直接影响Agent的行为边界。
2.2 环境:Agent面对的不是物理世界,而是接口世界
PEAS里的环境,指的是智能体工作所处的世界。对扫地机器人来说,环境是一间真实的屋子,有墙、有家具、有灰尘。对LLM Agent来说,环境是一个接口世界:工具API、数据库、知识库、网页内容、其他内部系统,甚至正在和你对话的用户本身。
这个区别带来的直接后果是,LLM Agent的环境描述必须显式地写成提示词。模型看不到你系统里有哪些服务,它只能通过你提供的工具清单和环境说明来"理解"世界。你在工具描述里写了什么、漏了什么,直接决定了它认为自己在面对一个什么样的环境。比如你有一个天气查询工具,但如果描述里没有说明"只能查询未来三天"的约束,模型就会在用户问下周天气时继续调用这个工具,拿到空结果后懵掉。
我在实际设计中还有一个体会:环境要尽量做减法。很多人给Agent一口气接上十几个工具,觉得能力越多越强,结果模型在十几个相似工具里频繁选错。环境这东西,不是越丰富越好,而是越清晰、越可预期越好。就像人到一个陌生的城市,手里有一张重点标注的地图,比拿到一本厚厚的地图册更容易不迷路。
2.3 执行器与传感器:函数调用与观察结果如何落地
执行器和传感器是智能体与环境交互的两只手。经典定义里,执行器改变环境状态,传感器从环境读取信息。在LLM Agent里,执行器的实现载体是函数调用和API请求,传感器的载体是函数返回值和外部数据源返回的文本内容。
这里有一个经常被忽略的设计细节:传感器的"感知带宽"有多宽,取决于你给模型返回了什么。一次工具调用的返回值如果是几千行的JSON,模型很可能被噪音干扰,抓不住关键字段。更好的做法是让工具返回值只包含决策所需的信息。比如查询用户订单,工具可以直接返回"订单X在运输中,预计明天送达",而不是把整条数据库记录原样丢给模型。
执行器方面,核心是可观测性和可回滚性。函数调用一旦发出,可能会改变真实世界状态——发了一条消息、提交了一笔订单。所以在设计执行器时,要给关键动作加上日志和确认机制。在我的项目里,凡是涉及写操作的执行器,都会在工具层打一条结构化日志,记录"谁、什么时间、调用了什么、传了什么参数、结果如何"。这样即使后续Agent行为异常,也能倒查问题。
2.4 别把PEAS当实现蓝图:它是一张分析体检表
学习PEAS时最容易犯的误解,是以为照着四个维度填空就能得到一个可运行的Agent。实际上PEAS是一个描述性、分析性框架,它的作用是帮你在设计阶段看清边界、发现矛盾,而不是告诉你代码怎么组织。它更像体检表,检查你的Agent有没有考虑性能标准、有没有明确环境假设、执行器和传感器是否匹配。
我见过一个团队设计客服Agent,PEAS里的环境写的是"用户会通过网页聊天窗口提问",但执行器清单里却没接任何能查询工单系统的工具。结果模型只能靠训练时的记忆瞎答。拿起PEAS这张体检表一对照,问题立刻暴露:环境里有"工单查询需求",执行器里却没有对应工具,感知和行动是断裂的。所以我的建议是,每做一个Agent,先花半小时把四个格子填满,再开始写代码。不用填得多复杂,但环境里提到什么信息源,执行器里就要有对应工具;性能度量里定义了"必须查数据库才能回答",传感器里就要保证工具真实返回数据。这个自查动作能避免一大半后期返工。
3. ReAct架构的核心循环:推理、行动、观察为什么缺一不可
如果说PEAS回答了"Agent是什么、该描述哪些要素",那ReAct解决了"Agent的决策循环到底该怎么转"。ReAct这个名字是Reasoning与Acting的合成,核心思想一句话概括:让大模型在思考轨迹和外部动作之间交错进行——先想清楚自己知道什么、缺什么,然后采取一个动作去补缺,观察动作结果,再继续思考。这个循环一直重复,直到任务完成。
3.1 思维链的"只看不做"困局
在ReAct之前,很多团队依赖思维链(Chain of Thought)来提升大模型的推理能力。思维链的套路很有效,它让模型把思考过程一步一步写出来,减少了"跳步"带来的错误。但它有一个致命短板:思维链的所有推理都发生在模型内部,不接触外部信息。模型只能基于训练时见过的数据去推理,遇到需要实时数据、最新状态、私有信息的问题,它唯一的选择就是编。
举例来说,你问模型"某商品今天是否还有库存",思维链可以让它分析"如何判断库存",但模型根本查不到当前库存表,推理再缜密也是空中楼阁。它可能会说"根据常见情况推测库存充足",这种回答在Demo里看着还行,放到生产环境就是事故。问题出在推理和行动是割裂的——模型只负责"想",没有人负责"做"。
ReAct的贡献就是把这两个环节焊在一起。它不让模型一次性输出全部思考,而是允许模型在推理的中途停下来、调用一个工具、看到真实结果、再接着推理。每一个推理片段都建立在最新观察之上,而不是建立在对世界的猜测之上。用打比方的话说,思维链像是闭着眼睛在脑海里走迷宫,ReAct则是每走一步就睁眼看一下路标。
3.2 Thought、Action、Observation三件套如何构成闭环
ReAct在工程实现上,通常表现为一个三步循环,对应模型输出的三种内容类型:
- Thought(推理):模型用自然语言描述自己对当前状态的判断、下一步的计划、选择某个工具的理由。这段推理文本的作用是让模型的决策过程可见、可审计。
- Action(行动):模型输出一个结构化的工具调用请求,包含工具名和参数。宿主程序负责解析这个请求,执行真正的函数。
- Observation(观察):工具执行完返回结果,作为新的一轮输入回传给模型。模型把观察结果纳入当前上下文,继续下一轮Thought。
这个循环的关键在于,Thought不是可有可无的装饰,而是模型的决策依据。我发现一个有意思的规律:当模型在Thought里写清楚了"用户需要今天的天气,我需要调用按日期查询的工具",它的参数生成会更准确;如果模型没有Thought直接输出Action,参数往往会出现缺漏或格式错误。推理文本本质上在帮模型稳定状态表征。
工程上实现这个循环并不复杂,核心代码甚至几十行就能跑起来。我在第5节会给一个最小实现。但这里想先强调循环的边界条件:什么时候该停?判断标准是模型的输出里没有Action、只有最终回复,说明它认为自己已经收集到足够信息、可以给出答案了。这个信号在代码里就是一个判断分支:响应中有工具调用就继续循环,没有就输出最终结果。
3.3 为什么循环一次,幻觉就少一分
一个ReAct循环最直观的效果,是让模型的每个关键论断都有观察作为锚点。模型说"该商品有库存"之前,它先看到了库存查询工具返回的"库存剩余12件"。这个锚点把模型的输出从"猜测"变成了"对事实的转述",幻觉空间被大幅压缩。
当然,这不是说ReAct能根除幻觉。它只能保证模型在调用工具后产生的事实性内容更可靠,如果工具本身返回的数据有误,或者模型在多个观察之间做归纳时出错,该幻觉还是会幻觉。但相比纯文本生成,错误至少可以被定位——你能在轨迹里看到它是在哪一步基于什么观察得出错误结论。这种可审计性对生产系统来说价值巨大,出了问题可以复盘,可以加规则,可以修工具。
实际使用中还有一个副作用带来意外好处:用户对结果的信任度提高了。当智能体生成答案时附带了自己的推理轨迹和工具调用记录,用户能看到"它不是瞎说的,它真去查了数据库"。我在某模拟项目中统计过,带观察轨迹的回答,用户的二次质疑率明显低于直接给结论的回答。这在客服、运维等场景里是非常实用的提升。
4. 支撑循环运转的底层零件:记忆、工具注册与安全边界
理解ReAct循环之后,很多人的第一反应是"这也不难"。但真正落地时你会发现,循环只是骨架,运转起来还缺几样"器官":记忆让Agent记得住上下文,工具注册表让模型知道有什么可用,安全边界保证它不闯祸。这三样东西没有装好,循环跑不了几步就会散架。
4.1 记忆体系:短期上下文与长期外部存储怎么配合
ReAct循环有一个天然问题:每一轮新观察都要拼接到历史消息里,对话越长,上下文越臃肿。这就要区分两类记忆。短期记忆是当前任务上下文,包括用户的原始诉求、模型产生的Thought、工具调用记录和Observation。短期记忆直接放在上下文窗口里,它的问题是有长度上限,而且随着轮数增长,token消耗越来越大。长期记忆则是Agent跨会话、跨任务需要保留的信息,比如用户偏好、历史工单、项目背景、之前Task的结论。长期记忆通常存放在外部存储中——文本摘要、结构化记录、向量检索库——需要时再取回。
我见过不少人把短期记忆和长期记忆混在一起,一股脑全塞进上下文,结果上下文长度爆炸,模型注意力被低频历史信息稀释,开始犯低级错误。正确的做法是分层管理:对话早期且已经被执行完的步骤压缩成摘要,只保留最近若干轮完整轨迹。比如,一轮十步的任务,可以把前七步压缩成"已完成天气查询与路线规划,结果经确认为可选A方案",后三步保留完整Tool Call细节。模型既知道进度,又不会被冗长的历史干扰。
长期记忆的取回时机同样重要。取回的信息不是越多越好,而是要和当前决策高度相关。拿客服场景举个例子:Agent查用户历史工单,如果返回最近二十条工单摘要,模型反而不知道该参考哪条;如果先做一次意图分类,只取和当前问题类型相关的三五条历史,效果就好很多。记忆系统的设计原则是为决策服务,不为存档服务——存下来很容易,能恰好在需要时取出来才是本事。
4.2 工具注册表:描述质量决定调用成功率
工具注册表是Agent"知道自己有哪些牌能打"的依据。对LLM而言,工具不是代码里的函数列表,而是一段结构化的自然语言描述。模型会根据描述决定该不该用、用哪个、传什么参数。所以工具描述的质量,几乎直接等于工具调用的准确率。
我在实际项目中整理了一份工具描述的检查项,照着填能避免大部分选错工具、参数缺失问题:
| 检查项 | 差的做法 | 好的做法 |
|---|---|---|
| 工具名 | handle、do_sth | query_order_status,动词+对象更清晰 |
| 功能描述 | "查询信息" | "按订单号查询订单当前物流状态,仅支持已发货订单" |
| 参数说明 | 只列出字段名和类型 | 注明必填/选填、取值范围、格式示例 |
| 边界约束 | 不写 | 说明数据覆盖范围、失效条件、哪些情况应拒绝调用 |
最容易翻车的点有三个。一是相似工具的区别没写清,比如"查询天气"和"查询历史天气"同时存在,描述里如果不说明一个查未来、一个查过去,模型就会随机选。二是参数格式示例缺失,模型可能把日期写成自然语言"明天",而你接口要求"YYYY-MM-DD",于是调用失败。三是错误返回的语义不明确,工具返回"ERROR"四个字母,模型根本不知道是参数错了还是数据不存在,下一步就是瞎猜。
我自己的习惯是,每个工具注册表里都要附带至少一个few-shot示例,展示正确的调用方式。特别是那些参数复杂、有前提条件的工具,示例比任何描述都管用。工具数量建议控制在20个以内,超过这个规模,模型的选择开销会激增,有必要做一次工具分组或前置意图路由。
4.3 白名单与确认机制:给Agent装上刹车
Agent手里拿着能调外部系统的执行器,就意味着它拥有了"改变世界"的能力。如果一个Agent能自动发送邮件、修改配置、提交订单,那么它的一次错误调用就可能是事故。安全设计的底线原则是:工具列表本身就是Agent的安全边界,凡是没注册的工具,模型一律碰不到。
在此基础上,我强烈建议给敏感操作加一道"确认闸门"。具体做法是把工具划分成普通类和敏感类。普通类(查询天气、读日志、检索文档)可以完全自动执行;敏感类(发送消息、写数据库、操作生产环境)则要求Agent先输出"行动计划",由人工或者规则引擎确认后才真正执行。
有人觉得这样会拉低Agent的自动化程度,我反而认为这正是Agent能上生产的前提。自动化追求的是处理常规任务的效率,而不是把决策权完全交给一个不可完全预测的模型。实际操作中,"确认闸门"可以做成异步的:Agent在需要敏感操作时先挂起,把计划以纯文本发到审批队列,人工点一下同意,系统继续执行。这个模式的自动化率仍然很高,但出大事故的概率急剧下降。
5. 一个最小可跑的ReAct循环:从模块拆解到完整验证
理论讲了不少,下面直接上个能跑的东西。我特意把实现压到最简,让你看清ReAct循环的内在结构。这个Demo不依赖特定厂商的API,只用抽象的llm_chat()函数来表示"调用任意一个大模型接口",工具也用一个纯Python函数来模拟。
5.1 核心循环的代码骨架
整个循环的思路是这样的:维护一个消息列表,把用户问题放进去;请求模型生成回复;如果回复里带有工具调用请求,就在本地执行对应的函数,把结果作为一条tool消息追加到消息列表;然后再次请求模型;直到模型回复里没有工具调用,就把当前内容当作最终答案输出。
import json # 模拟任意大模型API的封装 # 实际使用时替换为你所使用模型的SDK方法即可 def llm_chat(messages, tools): raise NotImplementedError # 模拟两个工具函数的真实实现 def query_stock(item_id): stock_map = {"A001": 15, "A002": 0} return stock_map.get(item_id, "未知商品") def calc_price(base, discount): return base * discount # 工具分发表:从工具名映射到具体函数 TOOL_IMPL = { "query_stock": lambda args: query_stock(args["item_id"]), "calc_price": lambda args: calc_price(args["base"], args["discount"]) } # 工具注册表:给模型的JSON描述 TOOLS = [ { "type": "function", "function": { "name": "query_stock", "description": "查询商品库存数量,返回数字;库存为0表示无货", "parameters": { "type": "object", "properties": { "item_id": {"type": "string", "description": "商品编号,如A001"} }, "required": ["item_id"] } } }, { "type": "function", "function": { "name": "calc_price", "description": "计算商品折后价格,base为原价,discount为折扣系数", "parameters": { "type": "object", "properties": { "base": {"type": "number"}, "discount": {"type": "number"} }, "required": ["base", "discount"] } } } ]然后是这个Demo的灵魂部分——循环体:
def run_agent(user_query): messages = [{"role": "user", "content": user_query}] for step in range(6): # 限制最大轮次,防止死循环 resp = llm_chat(messages, tools=TOOLS) assistant_msg = resp["message"] messages.append({ "role": "assistant", "content": assistant_msg.get("content") or "", "tool_calls": assistant_msg.get("tool_calls") }) # 如果没有工具调用,说明Agent认为可以给出最终答案了 if not assistant_msg.get("tool_calls"): print("最终回答:", assistant_msg["content"]) return assistant_msg["content"] # 逐个执行模型请求的工具调用 for call in assistant_msg["tool_calls"]: fn_name = call["function"]["name"] fn_args = json.loads(call["function"]["arguments"]) print(f"[第{step+1}步] 调用 {fn_name}, 参数:", fn_args) if fn_name not in TOOL_IMPL: result = {"error": f"未知工具: {fn_name}"} else: try: result = TOOL_IMPL[fn_name](fn_args) except Exception as e: result = {"error": str(e)} # 把工具执行结果作为observation追加到上下文 messages.append({ "role": "tool", "tool_call_id": call.get("id", "call_demo"), "content": json.dumps(result) }) print("达到最大轮次,任务未在限定步数内完成")这个实现虽然简略,但已经完整覆盖了Thought、Action、Observation三要素。模型输出的tool_calls是Action;工具执行返回的json.dumps(result)那条消息是Observation;而模型每轮回复里先写Thought再决定是否调用的过程,由模型自行生成。整个循环是否终止,取决于模型自己判断"信息够不够"。
5.2 用它跑一次"查天气并安排出行"的完整轨迹
纸上谈兵不如实际走一遍。假设模型已经接入了上面的循环,用户输入是:"商品A001还有货吗?如果有,按原价100打八折算一下到手价。"
模型第一轮生成的可能是这样的内容(下面是我在类似项目里见过的高频轨迹,Demo结构一致):
Thought: 用户先问库存,再问折后价,我需要先查A001库存,然后计算价格。 Action: query_stock(item_id="A001") Observation: {"result": 15}如果你在本地跑,会看到循环打印出"调用 query_stock, 参数: {'item_id': 'A001'}"。随后第二轮:
Thought: 库存为15,说明有货。用户要求按100元原价、八折计算,discount应为0.8。 Action: calc_price(base=100, discount=0.8) Observation: {"result": 80.0}第三轮模型看到两个观察结果后,已经没有未知信息,于是不再生成工具调用,直接输出最终答案:"A001有库存,共15件;按原价100元打八折后到手价80元。"
注意一个细节:模型在没有调用工具之前,完全没有办法知道库存是15还是0。它生成"查库存"动作的唯一依据,是Thought里对自己"信息缺失"的判断。这就是ReAct相比"一步生成答案"的核心差异。
5.3 把这个Demo扩展成生产系统要补什么
从Demo到生产,中间还差好几层工程活儿。首先模型调用要被可靠封装:llm_chat需要支持超时重试、并发控制、流式输出、不同消息角色的正确映射。其次工具执行要做沙箱隔离:如果Agent被允许执行代码或访问真实系统,工具函数必须放在独立的执行环境里,限制文件系统、网络、资源的访问范围。第三是日志与追踪:每一轮的Thought、Action、Observation都要结构化落库,形成一个task级的完整轨迹记录,这样既方便Debug,也方便后续回放和评估。
还有一点很容易被新手忽略:工具返回内容的安全过滤。工具拿到的数据可能包含来自外部的恶意字符串,直接拼进上下文再交给模型,可能引发提示注入攻击。我的惯例是,工具返回值一律转成JSON并做长度截断,绝不让工具返回里的"指令文本"有机会冒充系统提示词。
把这些补上之后,这个几十行的循环就会膨胀成一个有状态管理、有权限控制、有评测反馈的Agent服务,但核心仍然没变——还是那个Thought-Action-Observation的循环。
6. 落入真实项目前,绕不开的成本、稳定性与评估问题
很多团队做Agent Demo时很兴奋,跑几个漂亮案例就开始规划上线。但真实项目里的Agent不会只在干净的环境里运行,它会遇到上下文膨胀、死循环、模型乱调用这类实际问题。这些问题如果不提前想清楚,上线一周就会被逼着回滚。我在这节把踩过的坑集中说一下,按成本、稳定性、评估三个方向展开。
6.1 token成本会随循环线性膨胀,请做好预算
ReAct循环每多跑一轮,就要把此前全部消息重新送给模型一次,token消耗不是线性而是近似平方级增长。一次十轮的任务,消耗的token往往是一个五轮任务的三倍以上,而不是两倍。配置一个查库存再计算价格的小任务,用户可能只说了十几个字,最终却烧掉几千token,只是因为循环里反复携带了大量工具定义和历史观察。
省钱有几个实际手段。第一,工具定义不需要每轮都重发。有些API协议允许把tools参数单独设置,但在消息历史上,很多实现依然会重复发送,尽量用支持tools缓存或引用机制的调用方式。第二,历史消息压缩。前面讲过,把早期已完成的步骤压缩成一行摘要,能显著降低每轮开销。第三,区分规划模型和执行模型。规划模型用廉价快速的模型负责生成Thought和选择工具,执行模型只处理被选中的具体动作参数,这种分层架构成本能压到原来的几分之一。
成本预算上,我建议每个Agent任务上线前先跑三个典型场景,统计每类场景的平均轮数和token数,再乘上预期的任务量,得到一个基准线。后续如果发现token突然翻倍,多半是循环逻辑出了偏差,比如模型在反复调用同一个查询工具,这时候该排查的不是费用,而是工具描述或上下文里有没有误导信息。
6.2 死循环与过调用:最大步数以外的防御手段
ReAct循环最经典的故障就是"死循环"——模型反复调用同一个工具,或者连续调用功能相似的几个工具,始终得不到能推进任务的信息,于是又发起同样的请求,周而复始。最简单粗暴的防御是设一个最大循环次数,比如6到10轮,超出就强制终止并让用户进一步澄清。但最大步数只是"最后一道保险丝",真要减少死循环,还得从源头找原因。
死循环的根因通常有三个。第一个是工具返回的信息不足以支撑决策。比如查询接口返回了"查询失败",但没说明为什么失败,模型只能再查一次碰运气。解决办法是让工具错误信息具备足够语义。第二个是工具描述导致错误选择。模型发现自己调用的工具不符合预期,退回来想找替代工具,找不到就轮番试。第三个是任务本身在当前工具集下无解。Agent没有能力获取某个数据,但任务又要求这个数据,于是反复尝试。
针对无解的情况,最好的做法是想办法让模型尽早承认能力边界,而不是硬试。我见过一个方案很有效:在系统提示词里明确写一句"如果尝试两个不同工具后仍无法获取必需信息,直接告知用户当前无法完成,并说明缺少的信息类型"。这句话把"承认失败"变成一个合法的终止分支,让Agent可以体面退出循环,而不是无脑空转。
过调用是另一个隐性成本黑洞,即模型为了一个简单问题连续调用多个工具,实际上第一个工具已经给出了足够信息。减少过调用同样要从描述和示例入手,在工具注册表的示例里展示"最简调用路径",同时设一个统一的自检规则:只有在消息历史中没有任何信息能回答用户当前问题时,才调用新工具。
6.3 评测与可观测性:没有反馈闭环的Agent是盲飞
最后一个容易在项目后期才暴露的问题,是怎么证明Agent真的"可用"?大模型输出有随机性,同一个任务今天能跑通,明天可能换一种做法。如果不建立评测机制,Agent就是盲飞状态。
我给Agent项目配评测集时,至少覆盖三类样本。标准路径样本:若干条预期在N步内完成的常规任务,用来验证基本能力不退化。边界条件样本:包括超出工具能力的问题、歧义提问、需要澄清历史的场景,用来验证Agent不会乱来。故障注入样本:人为让工具返回错误,观察Agent是否能合理应对。评测指标不单看最终回答正确率,还要看工具调用精确率——该调用的工具有没有调用、不该调用的有没有多调、参数是否每次都合法。我见过一个Agent最终回答正确率高达90%,但工具调用精确率只有60%,这意味着它有三分之一的操作是无意义甚至有害的,只是恰好没影响最终结果。
可观测性方面,我强烈建议从第一天就接入完整的轨迹日志。每轮循环至少记录:时间戳、轮次序号、Thought原文、Action的工具名与参数、Observation的返回摘要、本轮token消耗。把这些日志结构化存起来,后续既能做离线评测,也能在用户投诉时快速定位原因。没有反馈闭环的Agent系统,出问题时排查效率会低到让人崩溃——你只知道它给了一个错误答案,却完全不知道它当时在想什么、做了哪几步、在哪一步开始偏离。
做Agent项目这两年,我最大的体会是:架构上的"智能"其实没那么玄,PEAS和ReAct所提供的,本质上是把不可控的大模型封装进一个可控的决策框架里。框架负责纪律,模型负责灵活,两者配合,才能让Agent既聪明又可靠。最后分享一个小技巧:新写的Agent别一上来就接太多工具,先用一两个工具把一个完整任务跑通、跑稳、跑出日志,再逐步扩能力面。先学会走,再学跑,这条路在Agent工程里同样成立。