做Agent相关项目有一段时间的人,应该都有同一种感受:最早写提示词,是在教模型“怎么回答”;后来写工具调用,是在教模型“能做什么”;现在做决策模型,是在教模型“什么时候做什么、做完之后又怎么办”。这个标题看着像课程里的一小节,但它其实把Agent工程化的核心问题给点出来了——怎么让一个基于大语言模型(LLM)的系统,在面对不确定的真实请求时,自己拆解任务、调用工具、评估结果,并决定下一步动作。这篇文章就围绕“Agent决策模型从理论到实践”展开,适合正在做AI应用落地、想让智能体从“聊天机器人”进化到“能干活”的开发者参考。
我会先从决策模型的设计思路入手,讲清楚为什么不能指望一次提示词搞定所有事情;然后拆解感知、决策、行动、反馈这四个关键环节;接着给出一套可以直接落地的代码骨架和参数配置;最后把我实际调试中踩过的坑、排查过的典型问题整理成一份“实录”。内容偏工程实践,理论部分只保留能指导选型和写代码的部分,不堆名词。
1. 内容整体设计与思路拆解:为什么智能体必须有一个“显式决策层”
1.1 从“单轮对话”到“自主行动”的本质变化
先看一个最朴素的场景。用户说“我想查一下上个月的订单,最好把收货地址也改了”,如果你只是做一个对话机器人,模型会给你一段话:好的,您的订单编号是XXX,我建议您联系客服修改地址。但用户要的不是建议,是“把事办完”。
想真正把事办完,系统就得具备一个决策闭环:确认用户意图、查询订单状态、判断地址是否可修改、调用修改接口、把结果反馈给用户。每一步都不是简单的“生成文本”,而是一个基于当前状态的选择问题。这时候仅仅靠“大模型 + 一堆函数定义”是不够的,因为模型不知道该在什么条件下、以什么顺序调用哪个函数,也不知道调用完以后结果是否符合预期,更不知道失败后应该重试、换路还是直接求助人工。
所以要在应用层引入一个显式的决策模型。这个“显式”指的是:决策过程不再隐式地分散在用户和模型的对话里,而是被结构化地表达出来——有状态、有步骤、有条件分支、有终止规则。我自己的体会是,这个转变是Agent项目的分水岭:没有显式决策层,智能体只是一只有嘴的鹦鹉;有了它,才开始像一个能独立完成任务的执行者。
1.2 主流的决策范式:从ReAct到Plan-and-Execute,各有各的脾气
过去一年多,业界出现过很多决策范式,最有代表性的几个我列在下面,附上我自己的适用场景判断。
| 决策范式 | 核心思路 | 适合场景 | 主要坑点 |
|---|---|---|---|
| ReAct(思考-行动-观察) | 每步先输出推理文本,再决定调用什么工具,观察结果后进入下一轮 | 工具数量适中、任务链条中等长度 | 容易绕圈;上下文消耗快;对模型推理能力要求高 |
| Plan-and-Execute(先计划后执行) | 先让模型生成完整计划,再逐项执行并校验 | 步骤清晰、流程固定的任务 | 计划错误时全盘错;对动态变化的场景不敏感 |
| Reflexion(反思与重试) | 在失败后让模型自我复盘,生成修正意见后重跑 | 需要反复试错才能成功的任务 | 重试次数越多成本越高;反思质量不稳定 |
| 多智能体协作 | 让多个角色各管一段,互相评审 | 任务复杂度高、需要分工 | 通信成本成倍增加;收敛困难 |
如果你刚开始做一个Agent,没有充分理由就别上多智能体。我见过不少项目,一个任务被拆成“主管”“执行者”“评审者”三个角色,结果大模型之间互相甩锅,日志看得人脑壳疼。从ReAct起步、在必要的环节引入计划前置,是好选择。原因很简单:ReAct的容错性最好,每一步都有观察反馈,不会因为一次规划错误导致全局崩盘;同时它的实现成本也最低,一个while循环就能跑起来。
1.3 好决策模型的五个设计原则
基于这些对比,我总结出五个对实际工程最有用的原则,后面所有代码和配置都围绕它们展开。
第一,闭环必达。每一步必须包含“执行-观察-判断”,不允许只执行不看结果。第二,可回退。任何一个分支都要有fallback路径,比如工具调用失败后是换工具还是问用户,必须在设计阶段就写清楚。第三,状态可见。Agent内部的当前意图、已收集参数、未完成步骤,都必须能被外部读取,否则出了问题无从排查。第四,成本可控。决策步数要有上限,大模型调用要尽量省。第五,尊重用户在场。关键操作前要确认,高风险操作要让用户知情,不能为了让Agent显得“智能”就悄悄替用户做决定。
为什么把“尊重用户在场”也列为原则?因为在真实生产环境里,用户信任和合规要求往往是Agent能否上线的前提。一个能自动改地址但改错的Agent,比一个会先问一句“您确认要修改吗”的Agent,事后处理成本高得多。决策模型的本质不是“替人做选择”,而是“在合适的时间让合适的人参与选择”。
2. 核心细节解析与实操要点:感知、决策、行动、反馈四层拆开揉碎
2.1 感知层:把“一句话”变成“结构化状态”
决策模型要靠谱,第一步不是让模型想,而是让它“看清楚”。很多Agent效果差,问题不在推理,而在感知阶段就把信息搞丢了。
我建议把所有输入都做一层结构化转换。用户说“帮我查一下上个月的订单,最好把收货地址也改了”,这里至少有三种信息:用户意图(查询+修改)、实体对象(订单)、附加条件(上个月、收货地址)。如果直接把这句原文丢给决策循环,模型每次都要重新理解一遍,既浪费token又容易前后不一致。
实操上,我会在前置模块里用一次轻量模型调用完成信息抽取,输出一个JSON结构作为Agent的初始状态:
{ "intent": "modify_order_address", "slots": { "order_time": "last_month", "operation": "check_and_modify", "target_field": "shipping_address" }, "missing_slots": ["order_id", "new_address"], "confirmed_items": [] }注意这个JSON里有个字段叫missing_slots,这是整个感知层设计里最关键的一点。它告诉决策循环:哪些信息还缺?下一步的动作就应该是“向用户追问缺失参数”,而不是直接调用查询工具。很多Agent跑飞,就是因为感知层没有把“已有什么、缺什么”说清楚,导致模型瞎猜参数。
补充一句,信息抽取失败时不要硬抽。如果置信度低于阈值,感知层应该直接输出“需要用户澄清”的状态,宁可多问一句,也不要带着错误参数往下走。
2.2 决策层:LLM的“路由+规划”双重角色
决策层是整个Agent的大脑,它要同时做两件事:路由和规划。
路由指的是判断当前该走哪条路:是追问澄清、调用工具、给出答案、还是求助人工。规划指的是在选定路由后,设计具体执行步骤。这两件事如果混在一次提示词里,模型容易顾此失彼。我的经验是把它们分开:先用一个简短的分类提示词确定路由,再为需要规划的路径单独生成步骤。
比如当前状态是missing_slots非空,路由结果就应该是ask_user,根本不需要规划;如果状态变成了“订单查询成功,地址在可修改窗口内”,那么路由结果才是call_tool,这时再进入规划环节。
路由阶段我常用的提示词模板长这样:
你是智能体的决策路由模块。根据当前状态,从以下选项中选择最合适的一个动作: 1. ask_user: 需要用户补充信息或确认 2. call_tool: 需要调用外部工具继续执行 3. answer: 已有足够信息,可直接回复用户 4. escalate: 遇到无法处理的情况,转交人工 当前状态: {state_json} 请只输出动作名称和一句简短原因,不要输出其他内容。这个设计的好处是每个动作都轻量快速,延迟低,还能把“复杂决策”拆成“简单判断+局部规划”,避免模型在一次输出里夹带过多私货。
2.3 行动层:工具调用的“契约思维”
工具调用是Agent的手和脚。这里最容易犯的错误是:只给模型一堆函数名和描述,却没有一个严格的调用契约。结果模型生成了参数,但参数里缺字段、类型不对、甚至调用了完全不该调用的工具。
我强烈建议所有工具都采用强类型schema管理,并且在运行时做校验,不要信任模型输出。用一个比较朴素的伪Python来说明:
from pydantic import BaseModel, Field class OrderQueryParams(BaseModel): order_id: str = Field(description="订单编号,以PO开头") detail_level: str = Field(default="basic", pattern="^(basic|full)$") class OrderModifyParams(BaseModel): order_id: str new_address: str user_confirm: bool = Field(description="用户是否已确认修改")每次模型决定调用工具时,先拿它生成的参数去解析这个BaseModel,解析失败就返回明确的错误信息给模型,让它重新生成。这比直接把参数透传给业务接口安全得多,因为大模型漏参数的比率远比你想象的高,尤其字段多的时候。
另外一个心得是:工具描述要写“什么时候用、什么时候不用”,而不是只写“这个工具能做什么”。反例是“查询订单信息”,正例是“当用户提到订单、物流、收货信息且已有PO号时使用;当用户只是泛泛而谈咨询时不要用”。这个说明直接影响路由准确性,值得花时间逐字打磨。
2.4 反馈与记忆层:让Agent不白干活、不白犯错
反馈层做的事,是决定Agent能不能持续变好的关键。这里包括三步:结果校验、短期记忆更新、经验沉淀。
结果校验是在业务返回后做的自动检查。比如查询订单返回的订单状态是“已取消”,Agent原本准备去改地址,这就出现了状态与目标冲突,必须触发分支调整。我建议每个工具都绑定一个validator函数,返回ok或带有机读错误码的失败信息,这样决策循环才能做出正确判断。
短期记忆是把当前任务上下文写进状态对象,比如查到的订单编号、当前已确认的信息、已经执行过的动作序列。特别注意不要把所有对话历史都塞给模型,要把“历史对话压缩成状态摘要”和“保留最近两轮原始消息”结合使用,平衡理解力和token成本。
经验沉淀是更高阶的玩法,也就是常说的“反思模板”。当任务失败或者用户不满时,把“目标-动作-观察-失败原因”记录成一条结构化的反思,下次类似场景通过few-shot方式注入提示词。我实际操作下来,这个对减少重复性错误很有效,尤其是工具参数错误和场景识别错误那两类。
3. 实操过程与核心环节实现:搭建一个可运行的决策循环
3.1 明确我们要做什么:一个“售后工单处理Agent”
为了不空谈,我设计一个足够典型、又不会让读者陷入业务细节的Demo:一个“售后工单处理Agent”。它要处理三类请求:查询订单、修改收货地址、申请售后补偿。边界是:只能处理已登录用户本人的订单;修改地址必须在发货前;补偿金额超过阈值必须转人工。
这三个业务约束会让决策模型变得很有意思,因为它们迫使Agent学会“拒绝”“确认”和“升级”这三个容易做不好的动作。最初可以不做登录和权限,但建议把“用户身份”放在状态里,哪怕是写死的测试用户,后面接真实系统时不用改架构。
3.2 状态对象:决策循环的地基
在写循环之前,我会先把状态对象定义出来。这个对象贯穿感知、决策、行动三层:
@dataclass class AgentState: user_id: str = "" intent: str = "" slots: dict = field(default_factory=dict) missing_slots: list = field(default_factory=list) history: list = field(default_factory=list) pending_confirm: bool = False step_count: int = 0 status: str = "init"status字段取值为init、running、need_user_input、done、error、escalated。每次循环开始前,决策路由先读这个状态;每次循环结束后,状态被更新。核心逻辑就是:状态驱动路由,路由改变状态,直到进入终止态。
3.3 主循环代码骨架:跑通ReAct的最小实现
下面是一份可以直接跑的最小决策循环,我把结构和注释写得很明确。注意它并没有绑定任何具体的大模型SDK,你可以无缝换成自己项目里封装好的调用函数。
def agent_loop(initial_state: AgentState, max_steps: int = 8): state = initial_state state.status = "running" while state.step_count < max_steps: state.step_count += 1 print(f"[STEP {state.step_count}] status={state.status}") # 1. 路由判断:根据当前状态确定动作方向 route = decide_route(state) if route == "ask_user": # 生成澄清问题,并把状态切到等待用户输入 question = generate_clarify_question(state) state.status = "need_user_input" return {"action": "ask_user", "question": question} if route == "answer": # 直接生成最终答复 answer = generate_final_answer(state) state.status = "done" return {"action": "answer", "text": answer} if route == "escalate": state.status = "escalated" return {"action": "escalate", "reason": state.last_error} if route == "call_tool": # 2. 规划:决定调用哪个工具、参数从哪来 plan = plan_next_action(state) tool_name = plan["tool"] arguments = plan["arguments"] # 3. 执行前校验,尤其是有敏感影响的操作 if require_safety_check(tool_name): state.pending_confirm = True return {"action": "confirm", "confirm": f"即将执行{tool_name}: {arguments}"} # 4. 调用工具并观察结果 observation = execute_tool(tool_name, arguments, user_id=state.user_id) apply_observation(state, observation) # 5. 判断是否需要再次确认或已可结束 if state.pending_confirm and observation["confirm_needed"]: continue # 下轮进入追问 else: # 会话外部输入触发的额外动作,真实场景可挂多渠道接入 state.status = "error" return {"action": "error", "error": f"unknown route: {route}"} # 超过步数强制收口 state.status = "error_halt" return {"action": "escalate", "reason": "max_steps_exceeded"}这段代码的核心思想是“每一步都产生一个可被外部感知的动作”,要么问用户、要么调工具、要么给答复、要么转人工。循环天然支持带确认的任务,因为pending_confirm会让Agent在下一步重新评估是否已获得用户授权。
3.4 关键参数怎么定:temperature、max_steps、工具数量
参数配置是这个模型能不能稳定工作的直接变量,我逐一说我的调参经验。
temperature在规划、路由、工具参数生成这些环节,我都会设为0或接近0。决策模型要的是可重复性,不是发散性。如果某条同样的任务跑五次每次路径都不同,线上监控会很难做。但如果你把“反思细化”出来的候选方案喂给另外一个创意模型,那个模型可以把temperature调高一点,两者分工。
max_steps建议初始设为6到8。小于4往往不够跑完一个“查询-确认-修改”的闭环;大于10则需要警惕失控风险。它本质是成本护栏,宁可让它超限转人工,也不要无限循环烧token。
工具数量也很讲究。单个决策模型同时能稳定选择的工具,我认为在5到15个之间比较合适。少于5个,很多任务需要你写很长的if-else,不如直接上决策模型;多于15个,模型开始频繁选错工具,你需要考虑做“工具分组”或“多级路由”。有一个做法很实用:把工具按域拆成“查询域”“修改域”“客服域”三组,路由模型先选组,组内再选具体工具,准确率会明显提升。
3.5 完整案例推演:一次“查询+修改地址”的决策流转
下面用表格推演一个具体请求在系统里的完整流转,帮你理解状态机和决策循环是怎么配合的。输入是:“我想看看上个月的订单,收货地址好像填错了,能改一下吗?”
| 步骤 | 感知/路由输出 | 状态变化 | 动作 |
|---|---|---|---|
| 前置 | 抽取意图=modify_order_address,缺order_id/new_address | status=running, missing_slots=[order_id, new_address] | 无 |
| 1 | 路由=ask_user | 生成追问:“请提供订单编号和希望修改的完整新地址” | 问用户 |
| (用户回复) | 补充order_id和new_address,missing_slots清空 | status=running | 继续循环 |
| 2 | 路由=call_tool,工具=query_order | 用order_id查询订单,得到状态state=created | 调工具 |
| 3 | 校验:created状态允许改地址 | 记录上一动作结果,无阻塞 | 无 |
| 4 | 路由=call_tool,工具=modify_address | pending_confirm=True | 请求确认 |
| (用户确认) | 用户说“确认修改” | pending_confirm=False,missing_slots保持空 | 继续循环 |
| 5 | 路由=call_tool,执行modify_address | 返回成功,status=done | 返回“修改成功,请留意后续物流通知” |
注意第4步第5步拆开的安排:即使模型已经知道用户本来就想改地址,我依然会让它在真正执行前确认一次。原因很简单,模型可能会错意,用户也可能是随口一说。吃一次误改地址的投诉之后,你会感激这个多余步骤。
4. 常见问题与排查技巧实录:踩过的坑、修过的问题
4.1 决策循环死循环:Agent反复调用同一个工具
这是最高频的故障。表现为日志里同一个工具被连续调用六七次,参数几乎一样,观察结果也一样,但模型就是不结束。
我第一次遇到时也排查了很久。后来发现根因往往是两个:一是工具返回的观察结果没有发生状态变化,模型认为任务还没完成;二是路由提示词里缺少“这个结果已经可以回答用户”的判断示例。
解决办法有两个层面。代码层面:在工具执行后增加“结果去重校验”,如果当前观察与前一步完全相同,就让路由进入ask_user或answer分支。提示词层面:在路由说明里显式加入一条:“如果当前状态显示核心目标已达成,直接选择answer,不要尝试调用额外工具。”
注意:不要只加“最多调用N次”这种硬限制。硬限制能止损失,但用户看到的是“抱歉,我无法完成”,体验很差。真正该做的是让模型理解“什么时候该停”。
4.2 模型幻觉工具:生成了不存在或未授权的函数
现象是模型输出一个看起来合理但根本不在工具列表里的函数名,比如check_user_logistics。原因是工具描述覆盖不全,模型试图创造性发挥,或者多级路由里组选择错误。
排查方向:先看这轮路由前的状态和工具列表。如果是单级路由,大概率是工具描述写得不够“割裂”——几个工具之间功能重叠,模型分不清边界。解决方法是把工具描述写成“互斥判定”风格:有明确的使用门槛和排除条件;同时在执行层必须校验函数名白名单,不在白名单内直接拒绝,并返回错误让模型重新规划。
4.3 上下文窗口吃紧:长对话任务中途开始退化
Agent决策质量在第六七轮后会明显下滑,这是上下文污染导致的。表现为模型开始忽略早期信息、回答变空洞、工具参数开始出错。
我在这里吃过亏,早期方案是把所有历史都拼接进上下文,结果又慢又贵。后来改成“滚动摘要”策略:
- 每完成一个子步骤,就把“步骤动作、关键观察、当前状态”压缩成一条结构化摘要;
- 上下文只保留最近两轮原文 + 全部历史摘要;
- 摘要的生成使用一次独立的轻量模型调用,而不是自己拼字符串。
这样操作后,上下文占用能减少约六成,决策质量也能稳住。但也提醒一点:摘要本身会丢失细节,所以在摘要里至少要保留“缺失参数列表、已确认事项、业务关键数字”这三类不可丢失的信息。
另外一个实用小技巧是:一旦状态变成need_user_input,直接把当前完整状态序列化到审计日志里,并把本次会话的模型上下文清空,等用户回复后再重新构造上下文。因为用户间隔几小时才回复的情况很常见,保留超长历史没有任何意义。
4.4 成本与延迟失控:Agent比人工还贵
常有人问,为什么跑一个Agent感觉API账单涨得飞快。原因通常是:一次用户请求背后,可能有路由调用、规划调用、生成问题调用、结果评估调用,再加上重试,四五次模型调用是常态。按每次调用几千token算,单任务成本确实不容忽视。
我的优化思路按收益排序:
第一,路由和工具参数抽取用轻量模型,最终答复可以由更强模型负责,分级混合调度。第二,规划阶段启动“缓存”:相同意图和相似状态直接复用上次成功路径,不必重复规划。第三,把重试限制从默认的3次降为1次,失败后快速转人工,省下的钱远超“多试一次可能成功”的期望收益。第四,对大段工具结果做预截断,只把关键字段送给模型。比如订单工具返回几十个字段,实际决策只需要五六个,我在工具层直接映射成精简要给模型的文本。
4.5 可观测性:没有链路日志,决策问题无从排查
最后一条建议,可能救命:一定要从第一天就给每个决策步骤打结构化日志。至少记录以下字段:
{ "agent_run_id": "run_20250101_001", "step": 3, "route": "call_tool", "tool": "query_order", "input": {"order_id": "PO123"}, "output": {"status": "created", "allow_modify": true}, "state_before": "...", "state_after": "...", "cost": "0.003", "latency_ms": 1200 }在调试循环类问题时,我会把这些日志串成一个时间线来观察。如果用户在抱怨“Agent乱来”,前端点开这条请求的链路日志,几乎一眼就能定位是路由错了、工具参数错了、还是状态更新漏了。没有这套日志,就相当于摸黑排查——你会花五倍的时间,最后还只能靠猜。
我个人的习惯是额外记录每个决策步骤的“重放缓存”,也就是把模型输入和输出原文存下来。这样即使模型提供商更新了版本、行为变了,也能对比排查是不是“模型自己变了”导致的线上抖动。
做到这一步,决策模型不但是“会思考的智能体”,还是一个“每个思考动作都有档案可查的智能体”。最后再分享一个实际经验:Agent跑得稳的团队,往往不是给模型更多自由度,而是给模型划了非常清晰的边界,该确认时确认,该停下时停下,该拒绝时坚决拒绝。把这条想透了,你的决策模型就成功了一大半。