做 Agent 快两年了,我最大的感受是:这个领域最不缺的就是概念和框架,最缺的是有人把“一个自主系统到底怎么从感知走到行动”这件事讲明白。前阵子某公司的内部项目要做一个智能助手,需求方最初的描述特别简单——“像聊天机器人一样,问答就行”。等真动手拆完需求才发现,用户要的是让它自己查数据、自己判断异常、自己换条件重试、最后把结论写到文档里。这一连串动作,恰恰就是标题里那件事的核心:AI Agent 不只是会“说话”,而是要从感知环境、规划决策、执行行动,再到接收反馈形成闭环,最终构成一个能独立完成目标的智能自主系统。
这篇文章我想按自己实际做项目时的思路来写:先把 Agent 的本质拆开,再说感知层怎么设计、决策层怎么规划、行动层怎么闭环,最后给出一条可以直接照着搭的最小实现路径和一堆踩坑记录。适合两类人看:一类是 Agent 概念听得很多、但还没搞清楚它跟聊天机器人到底差在哪的读者;另一类是准备上手做 Agent 系统、但不知道第一行代码该从哪落地的开发者。不用被市场上各种花哨名词吓住,一个能跑的 Agent 原型,比想象中简单,也比想象中更需要工程细节。
1. Agent不是“聊天机器人换皮”:自主系统的真实构成
1.1 会造句不等于会干活
很多人对 Agent 的理解,是把一个更强的模型塞进现有聊天框里——换个说法而已。但真的跑过一遍就知道,聊天机器人和 Agent 是两种完全不同的东西。聊天机器人做的是“一句话进来、一句话出去”,它改变的是对话本身;而 Agent 改变的是真实世界的状态——它要去查数据库、调接口、写文件、发通知,然后根据结果决定下一步干什么。
我常用点外卖来打比方。聊天机器人是一个特别会描述菜品的大厨助理,你问它“今晚吃什么”,它能给你报一长串菜名和营养分析,但它不会真的帮你点餐。Agent 则是那个跑腿小哥:接了“帮我点一份不辣的川菜”这个目标之后,会去看哪家店还开着,根据评价挑一个,下单,等出餐,取到之后核对一下,真送错了还会拿回去换。关键差异不在“谁更聪明”,而在“有没有行动能力”和“能不能根据环境反馈调整自己的动作”。
这个区别在实际项目里会直接反映在系统设计上。聊天机器人只需要做“模型 + 提示词”两层;Agent 至少要做“感知层、决策层、行动层、反馈层”四个部分。你让模型直接回答“上个月销售额是多少”,它大概率会编一个数字出来;但如果你给它接一个订单查询工具,让它查完之后再回答,数字才真正可信。这也是我判断一个系统是不是真 Agent 的标准:它会不会因为一次失败的调用而改变自己下一步的计划。
1.2 最小可用自主系统的三层骨架
一个最小可用的 Agent 系统,无论外面包装成什么样,内核都可以拆成三层:
- 感知层(Perception):负责收集环境信息。包括用户输入的原始指令、工具的返回结果、系统当前的状态、以及历史上已经完成过哪些步骤。感知层的产出是一份“结构化的上下文”,供决策层使用。
- 决策层(Decision):负责推理和规划。它拿到感知层给的上下文之后,要先判断“当前目标是什么、已经做到哪一步、有哪些可选工具、哪个动作最可能推进目标”,然后输出一个明确的下一步动作。
- 行动层(Action):负责把决策变成真实操作。比如调用某个接口、写入某个文件、发出一条通知。行动完成后,新的环境信息会再次进入感知层。
整个系统跑起来就是一个循环:感知 → 决策 → 行动 → 新感知 → 再决策……直到目标完成,或者到达我们设定的最大步数上限。下图不用画,用文字描述就是上面这三层首尾相接。
这个骨架的工程意义在于每一层都可以独立替换。今天用模型 A 做决策层,明天觉得推理不够强,可以换成模型 B,感知层和行动层不用动;后天要给系统加一个查库存的工具,只需要在行动层注册一个新接口。我一开始做的时候没有意识到这个好处,后来迭代了几轮才发现,正是这种“分层的松耦合”让 Agent 系统具备了持续演进的可能。
2. 感知不是“读文本”:Agent怎么把环境变成自己“看得懂”的输入
2.1 感知的核心是生成“环境快照”
Agent 的感知层如果不专门设计,很容易退化成“直接把用户的话拼到提示词里”,这是很多初版系统跑飞的原因之一。我在设计某内部工具时,会把感知层的输出固定成一种“环境快照”,里面始终包含四个字段:
- 当前目标:用户到底要达到什么效果,防止模型在长流程里跑偏
- 可用工具列表:这个 Agent 现在手上有什么牌可以打
- 上一步的结果:最后一次工具调用返回了什么,是正常结果还是错误信息
- 历史关键记忆:之前几步里沉淀下来的重要结论,比如“用户只看线下门店的订单”
举个例子,用户说“帮我查一下这个月订单有没有异常”。环境快照可能是这样:
目标:分析本月订单异常 工具:query_orders(按日期/门店查订单)、calc_delta(算差异)、send_notice(发通知) 上一步结果:query_orders 返回 0 条记录,可能日期范围填错 历史记忆:用户关注线下门店口径,不考虑线上渠道这个快照里没有任何多余的闲聊,全是决策层当下需要用到的信息。目标字段尤其重要,因为你一旦忘了把目标放进去,模型在几步之后就不知道自己在干嘛了,开始自己编任务,这是我踩过最深的一个坑。
2.2 记忆分三层,别把所有东西都塞给模型
大模型的上下文窗口越来越长,但它依旧不是无限大的。更重要的是,给模型的信息越多,决策的噪声就越大。我在项目里习惯把记忆拆成三层,各管各的:
| 记忆类型 | 存什么内容 | 典型形态 | 由谁写入 |
|---|---|---|---|
| 短期会话 | 最近两三轮的用户指令和回复 | 待处理的临时缓冲 | 感知层追加 |
| 任务状态 | 当前进度、已做步骤、失败原因 | 结构化 JSON | 决策层每次循环后更新 |
| 长期知识 | 用户偏好、业务规则、历史结论 | 向量库 / 知识库 | 定期总结后写入 |
这里最容易被忽视的是“任务状态层”。模型本身不存状态,它每轮看到的是我们拼给它的文本。所以 Agent 做了第一步、第二步,这些进度不能只靠模型自己“记得”,必须在环境快照里明确写出来。我见过一个 Agent 重复查询同一个接口三次,原因就是任务状态没有被写入下一轮感知,模型以为“自己还没查过”。
长期记忆的设计不用一上来就上向量数据库。早期的做法很简单:用一个 JSON 文件存关键结论,每轮循环就追加几条,用了很久也够用。向量检索是后面规模大了才需要引入的优化,不是起步的必需品。
2.3 感知的信息边界:太少了瞎子摸象,太多了自找麻烦
感知层有两个方向相反的坑。一个是“感知不足”:只把用户提问丢给模型,工具返回的错误信息不进上下文,模型根本不知道为什么刚才那个动作失败了,只会机械地重试同一个调用,浪费资源还无法推进。另一个是“感知过度”:把整个业务库的所有文档、所有历史日志一股脑塞进上下文,模型被大量无关信息淹没,选错工具的几率反而更高,Token 消耗也直线上升。
我的经验法则是维持一份“最少必要信息清单”,缺一不可、多则去重:
- 当前目标(不能省略,否则模型会飞)
- 最近一次工具结果原文或摘要(不能省略,否则模型不知道上一步发生了什么)
- 上一次决策的动作(用来对照,判断这个动作产生了什么影响)
- 历史关键记忆摘要(可以省略细节,只保留结论)
只要环境快照里包含这几项,感知层就算合格了。至于“该精取还是该摘要”,我的原则是:能摘要的绝不贴原文,能贴原文摘要的绝不贴全文。
3. 决策中枢:Agent的“下一步”是怎么算出来的
3.1 决策不是直接回答,而是“推理 + 选工具 + 定动作”
决策层是整个 Agent 系统里最像“大脑”的部分,但它做的事情不是“写回答”,而是“做决定”。模型的输出应该是这样一组内容:当前状态是什么、可选方案有哪些、我选哪个子动作、这个动作需要什么参数。如果判断目标已经完成,它也可以输出一个“结束”信号。
这个过程中有一个特别重要的设计选择:让模型一次性规划完所有步骤,还是让模型边做边规划?我强烈建议后者,也就是俗称的 ReAct 式循环——推理一步、行动一步、观察结果、再推理。原因很简单:真实环境充满不确定性。就像用导航软件,出发前一次性定死路线,遇到封路就傻了;边开边看的新方案是实时感知路况、发现前方堵了再改道,虽然每一步看起来不那么“高瞻远瞩”,但最终到达目的地的概率高得多。
早期我也试过让模型一次性输出“第1步做什么、第2步做什么、第3步做什么”,结果因为现实的工具返回常常和预期不一样,计划的第2步从一开始就建立在错误假设上。后来全部改成边看边做,系统稳定性明显提升。
3.2 工具选择的关键:把“工具说明书”写清楚
决策层要能选对工具,前提是感知层把工具的“说明书”完整地暴露给模型。每个接入 Agent 的工具都要注册一份元信息。下面是一个我常用的描述格式:
{ "name": "query_orders", "description": "按条件查询订单列表,返回订单号、金额、门店、日期。支持条件:date_from、date_to、store_id。注意:只能查询,不要用来做数据修改。", "parameters": { "type": "object", "properties": { "date_from": {"type": "string", "description": "开始日期,格式 YYYY-MM-DD"}, "date_to": {"type": "string", "description": "结束日期,格式 YYYY-MM-DD"}, "store_id": {"type": "integer", "description": "门店ID,可选"} }, "required": ["date_from", "date_to"] } }写工具描述这件事看着不起眼,实际影响巨大。我曾经在项目里用过一个描述写得特别含糊的工具:“按条件查询订单数据”,结果模型经常在需要查“退款单”的时候选到“订单查询”,因为描述里没有说清边界。后来我把描述改成“查询正常状态订单,不包含退款、不包含取消订单”,选错率立刻降了一截。描述越明确,模型越不容易自作主张。
行动层还要加一道参数装配的校验。模型返回的“action”里如果有缺失的必填参数、类型不对的字段、或者根本不存在的属性,行动层要拦下来,并把这个校验错误作为“错误信号”反馈给感知层。这一步不能省。没有校验的 Agent 就像不设卡口的收费站,什么车都能上高速。
3.3 让模型的输出保持结构化:思考与动作分离
决策层输出必须机读。我要求模型的输出严格分成两部分:Thought(推理过程)和 Action(结构化动作)。Thought 用自然语言写清楚“我为什么这么判断”,Action 用结构化格式表达“我要执行什么”。把思考过程和动作分开,最大的好处是出了问题能审计:当 Agent 做出一个明显错误的调用时,你可以回看它的 Thought,知道它是基于什么错误假设做了决定,而不是对着一个黑盒束手无策。
{ "thought": "上一步查询返回空,可能是日期范围填了下个月,调整为本月前几天再查", "action": { "tool": "query_orders", "params": {"date_from": "2025-06-01", "date_to": "2025-06-30"} } }再补充一个细节:模型输出的 JSON 不一定每次都合法。解析层要能容忍“模型输出了一堆散文,里面夹着一小段 JSON”的情况,用提取或正则把结构化部分捞出来。这个“解析纠错器”是我在项目第二天就加上去的,因为你没法指望模型次次都规规矩矩输出标准 JSON。
4. 行动与反馈闭环:自主系统不失控的关键
4.1 行动层要收敛到一个统一网关
行动层如果写成一堆散落的工具函数,后期必然后悔。我把所有外部调用收敛到一个统一行动网关里,每个工具的执行都要经过网关。网关统一做鉴权、限流、日志记录、异常包装。为什么非要加这一层?因为自主系统里模型自己决定调什么工具,如果每个工具的日志格式都不一样,出问题的时候你根本串不起一条完整链路。
行动网关至少要记录这些字段:
| 字段 | 说明 |
|---|---|
| 动作名称 | 调用了哪个工具 |
| 输入参数 | 模型当时传了什么 |
| 开始/结束时间 | 排查耗时用的 |
| 状态码 | 成功、失败、超时、限流 |
| 输出摘要 | 关键返回内容,太长就截断 |
| 异常信息 | 错误栈、错误码 |
这个表看起来琐碎,但它就是 Agent 系统的“黑匣子”。没有这层记录,Agent 表现再诡异你也只能靠猜。
4.2 失败反馈与自纠错:让错误重新进入决策
Agent 一定会失败。模型会幻觉调用不存在的工具,会把参数填错,会遇到限流和超时。真正的分水岭在于失败之后怎么办。最差的处理是“原样重试同一个动作到天荒地老”,我见过 8 次重试同一个查询接口的案例,纯粹烧钱烧时间。好一点的系统会把错误反馈重新送进感知层,让决策层看到“刚才因为什么失败了”,然后决定换一个工具、改一组参数、或者换个策略。
我建议在项目里做三级降级机制:
- 第一级:可重试错误(超时、限流、临时接口异常),带上错误信息原样重试,最多两次
- 第二级:让模型阅读历史步骤和失败原因,重新规划,换工具或换参数
- 第三级:达到最大步数或连续失败,停止自动执行,把中间状态转给人工处理
关键要区分“可重试”和“不可重试”。参数校验失败、业务逻辑错误、权限不足,这些重试一百次也是白搭,必须直接进入第二级甚至第三级。我在某内部系统里把默认最大步数设为 6,超过就转人工,既给了 Agent 足够的试错空间,又防止它在一个坑里无限打转。
4.3 安全护栏:别把大模型的“聪明”当成可靠
自主系统最怕“自作主张”,所以安全护栏不是加分项,是必需项。凡是高影响动作——删除、资金操作、对外发消息、改配置——必须在行动层设置人工确认点,或者按最小权限原则默认禁止。我习惯让 Agent 账号只拥有只读权限,需要写操作时强制走一次授权流程。
有次测试里,A同学 搭的 Agent 在理解用户意图时,把“测试环境”错误映射成了“生产环境”,差点去执行一个删除接口。幸好行动层做了接口白名单校验,生产环境的删除接口根本没注册给 Agent。这个例子很能说明问题:模型再聪明,工程护栏才是底线。自主系统的“自主”是有边界的,边界在哪里,必须在行动层用代码画死。
5. 从零搭出一个可从感知到行动的Agent原型
5.1 最小选型:不急着上框架
做 Agent 原型,我建议先不要引入任何重型框架。框架会帮你封装掉很多细节,但也正因为封住了,出问题时你很难看到内部发生了什么。最小方案只需要三样东西:一个支持工具调用能力的模型接口、两三个低风险查询类工具、一支 Python 脚本。
工具先别贪多,接两三个就够了。我第一版只接了一个订单查询和一个数值计算,先把“感知、决策、行动闭环”跑通了,后面再加工具都很顺。工具接太多反而让模型选择困难,前期大量时间都会花在“模型选错工具”的排查上。
5.2 主循环代码模板
下面是整个 Agent 原型最核心的主循环,去掉所有与具体供应商相关的细节,抽出来就是这个结构:
import json # 工具注册表:名称 -> 实际执行函数 TOOLS = { "query_orders": query_orders, "calc_delta": calc_delta, } def agent_loop(goal, max_steps=6): # 任务状态就是这段记忆的核心 task_memory = { "goal": goal, "steps": [], "last_result": None } for step in range(max_steps): # 1. 感知:把目标、可用工具、上一步结果拼成环境快照 context = build_context(task_memory) # 2. 决策:模型输出 thought + action(action 可能是 finish) decision = model_decision(context) action = decision.get("action") if action == "finish": return decision.get("answer", "目标完成") # 3. 行动:经过统一网关执行动作,得到新的观察结果 observation = execute_action(action) # 4. 把结果写回任务状态,作为下一轮的感知输入 task_memory["steps"].append({"action": action, "result": observation}) task_memory["last_result"] = observation # 日志落盘:每一轮的输入输出都要记录 log_step(step, decision, observation) return "达到步数上限,转人工处理"这个循环里,build_context是感知层,model_decision是决策层,execute_action是行动层。四步一个回合,循环直到结束:task_memory就是那一份任务状态记忆,它贯穿所有轮次,保证模型不会“失忆”。
这段代码没有依赖任何具体框架和库,你可以把model_decision换成任何你熟知的模型接口,把execute_action换成任何你想接的工具。先跑通流程,再谈优化。
5.3 三个递进验证实验:怎么确认它“活”了
代码能跑不等于 Agent 真的“活”了。我建议按下面三个实验递进验证:
| 实验 | 输入 | 通过标准 | 失败排查方向 |
|---|---|---|---|
| 实验一 | “查一下今天的订单数” | 模型选到查询工具并正确填参 | 工具描述是否清晰、决策输出是否可解析 |
| 实验二 | “对比上周和本周订单,算出差值” | 模型自动拆成查询+计算两步 | 任务状态是否在每轮正确写入 |
| 实验三 | 让接口故意返回限流错误 | 模型重试或换策略,而不是死循环 | 错误信号是否进入下一轮感知 |
实验一验证“选得对”,实验二验证“分得清”,实验三验证“扛得住”。三个都过,你的 Agent 最小闭环就成立了,之后加什么工具、换什么模型都是在这套骨架上的增量。
6. 实践中的坑与取舍:跑通容易,跑好需要费心思
6.1 感知层的典型坑:上下文爆掉、信息重复、错误信号丢失
上下文爆掉是最容易遇见的。模型上下文窗口再大,也顶不住你每轮都把“全部历史对话 + 全部工具返回”塞进去。解法不是加窗口,是减信息:只保留最近两轮原始对话,更早的内容压缩成要点;工具返回结果不需要全文进模型,只保留和任务相关的摘要字段。
信息重复是第二个坑。同一个工具返回的错误提示,会在每轮环境快照里反复出现,模型被“我失败了、我失败了、我失败了”刷屏之后,容易陷入复读机状态,每轮输出一样的错误决策。解法是去重:环境快照里只保留下一次最新的错误信息,历史错误列表单独存在任务状态里,不进模型,除非模型主动要求查看。
错误信号丢失是最隐蔽的坑。有些工具封装得比较粗暴,底层接口报 500,上层函数却只是返回一个空数组。Agent 看到“空结果”时完全不知道是“真的没有数据”还是“查询失败”,就会傻乎乎地把空结果当成正常结果继续往下走。解法很简单:行动层把所有异常显式包装成错误对象,写进last_result,让下一轮感知明确知道“这次查询没有正常结束”。
6.2 决策层的抽风:想太多、被描述误导、JSON 解析失败
模型“想太多”是个反直觉但常见的问题。你让它做一个简单的日期查询,它能给你规划出五步:查数据、算均值、画图表、写报告、发邮件。解决方案是在系统提示语里明确写明“能用一步完成就不要用两步,能用两次工具调用完成就不要用三次”,同时在决策层加一步动作审计:当连续多次动作都未改变任务状态时,主动降低下一步的复杂度。
工具描述误导导致选错,通常只能靠改写描述解决。一个通用的修法是:在描述里同时写“该做什么”和“不该做什么”。比如“query_orders:查询正常订单,不会返回退款单和取消单”。这句话看起来多余,但对模型选择正确工具帮助极大。
JSON 解析失败是每个 Agent 开发者都会经历的阵痛。模型输出里混着散文、代码块、以及残缺 JSON,解析器一崩整个流程就断了。我的做法是解析层做三层容错:第一次直接解析,第二次把输出里疑似 JSON 块提取出来再解析,第三次用正则补全缺失的引号和大括号。三个层级下来,能让成功率从八成提到九成以上。
6.3 成本和延迟:每一轮决策都在烧 Token,还用户等的着急
很多人做 Agent 时只关心功能,忽略了成本。算一笔账:一个三步任务,每一步决策的上下文大约 4k~8k token,三步下来就是 12k~24k token。这个数字再乘上重试次数,费用会非常可观。控制成本的核心是别让模型看到不必要的东西:历史记录用摘要替代原文,工具返回结果只保留关键字段,失败重试用短上下文而不是把之前所有轮次都塞回去。
延迟也是实际体验的大敌。Agent 的每一步都是一次模型调用,三步任务就是三次串行请求,用户端要等好几轮模型响应。缓解的办法有两个:一是让模型在决策时声明“这一步可以并行调用哪些工具”,行动层把可并行的调用并发执行;二是在前端展示中间状态,告诉用户“正在查询订单数据”“正在计算差异”,让等待变得可以接受。
6.4 从 demo 到生产还缺的几件小事
demo 跑通了,离能上生产还差几件看起来不起眼但绝对不能省的事:
- 持久化:任务状态不能只存在内存里,要落到数据库,进程重启之后还能恢复
- 审计日志:每一步模型的思考原文、调用的输入输出、错误信息都要落盘,这是排查问题的唯一依据
- 可观测性:统计每一步的耗时、Token 消耗、工具调用成功率,知道系统有没有在劣化
- 人工兜底:高影响操作的人工确认位、连续失败后的转人工通道
- 权限治理:Agent 账号按最小权限配置,只给完成任务所需的最小工具范围
这些事每件都琐碎,但每一件缺席都可能在某个深夜让你被线上问题叫醒。
写到这里,聊聊真实体会吧。我们那个内部项目,最早的版本特别“放权”,什么任务都让 Agent 自己决定。结果在一次测试里,它因为工具返回不完整,对同一个接口连着重试了八次,不仅浪费资源,还差点在系统里留下脏数据。后来我们把“可重试次数”和“人工确认点”当成第一等设计要素来规划,系统立刻稳了很多。
最后分享一个我认为最值钱的小技巧:Agent 每走一步,就把这一轮的“模型思考全文、工具调用输入输出、错误信息”存成一个独立文件,带上时间戳。这比任何日志框架都实用,排查问题时、给其他人讲解 Agent 行为时,你都能拿出完整的现场记录,而不是对着终端里被冲掉的输出回忆。再往下走,可以试试多 Agent 协作——一个拆任务、一个查数据、一个审核输出。但先把单个 Agent 的闭环跑扎实,那才是所有花哨玩法的基础。