← 回到知识图谱
概念持续完善

Agent 核心

本文总结定义、架构、Loop、规划、记忆、多 Agent:Agent 应用岗面试问得最多的部分。

创建 2026-10-31更新 2026-11-03阅读约 95 分钟
Agent面试Agent 核心

主线第 4 站 · 上一站:RAG · 下一站:工具/协议 · 全程:主线

本站在主线里回答什么:员工问:“我上周出差打车超标了,还能报吗?能的话帮我提报销工单。”走到这一站时,上下文里已经有用户身份(职级 P6,城市上海,第 2 站装配好的),也有第 3 站检索回来的条款:《差旅费管理办法》2026 版第 4.2 条(市内交通),写了交通标准和“超标需说明事由并经负责人审批”,带出处(条款内容为示意)。本站要定的是动作顺序和停在哪里。还缺两样信息:上周的打车发票,和有没有同一行程的报销单,那就去查。查到发票 INV-102,金额 118.5 元,标准 80 元,超标 38.5 元,判定为“可报,但要补事由”。建单是写操作,所以先把结论、依据和工单草稿给员工看,员工确认后才提出建单动作。信息不够就追问,证据冲突或超出权限就转人工,步数或预算用完就带着进度收口。

本站交给第 5 站的有两样东西。一是结构化的动作提议,说清调哪个工具、什么参数、依据和风险等级,大致长这样(示意):

{"tool": "create_reimbursement_ticket",
 "args": {"invoice_ids": ["INV-102"], "amount": 118.5, "over_standard": 38.5,
          "reason": "客户临时改了会面地点",
          "policy_ref": "《差旅费管理办法》2026 版第 4.2 条"},
 "risk": "write_reversible",
 "needs_confirm": true}

二是一份存在模型之外、可以恢复的任务状态。这个提议怎么安全地执行,是第 5 站的事。

先给结论

会输出工具调用的模型很多,能放心上线的 Agent 很少。差距出在后面那一截:它要在信息不全、工具会出错的环境里连着做十几个决定,每个决定都要能追溯,出了问题能停下来。面试时按这个顺序讲:定义、Loop、规划、记忆、多 Agent、失败兜底。

1. 定义与边界

一个工作定义:

Agent 是围绕目标,观察环境状态、选择动作、调用工具、根据结果调整后续步骤的系统。

workflow 里也可以用 LLM,所以“有没有 LLM”分不开两者。两者的分界线是谁决定下一步。拿报销案对照着看:

  • workflow:步骤由人预先编排,模型只填节点。比如查发票、比标准、建草稿这三步写死在代码里,LLM 只负责从发票图片里把金额 118.5 抽出来。
  • Agent:模型看了观测再决定下一步,运行时提供工具、状态和边界。比如 query_invoices 返回空列表,模型自己判断是该追问“是不是用个人账户打的车”,还是把日期范围放宽一天再查。
  • ChatBot:主要输出文本,不改变外部状态。比如回答“上海 P6 打车标准是多少”。
  • RAG:负责补知识,不负责多步行动。比如检索出第 4.2 条,照着条款回答。
  • Function Calling:模型输出“我要调 query_invoices,参数是……”的一种机制。它只是 Agent 的动作接口,本身不构成完整 Agent。

2. 架构与 Loop

一个完整 Agent 运行时通常包含下面这些组件。最后一列把报销案对上去,方便你面试时直接举例:

组件 作用 常见实现 报销案里是什么
模型 决策、规划、生成 LLM 判断“可报但要补事由”
Instructions/Rules 角色、边界、输出规范 system prompt、规则文件 “建单前必须确认”“不代替审批人”
Tools 动作空间 Function Calling、MCP、CLI search_policy、query_invoices、query_tickets、create_reimbursement_ticket
Memory/State 短期状态、长期记忆 会话、KV、数据库、向量库 任务状态 JSON;员工常驻上海
Planner 任务拆解、重规划 ReAct、Plan-Execute 依次查制度、查发票、判定、确认、建单
Runtime 执行、重试、超时、恢复 自研 Harness 发票接口超时重试两次
Guardrails 权限、审核、人工确认 RBAC、沙箱、HITL employee_id 从登录态注入,不让模型填
Observability trace、指标、日志 OpenTelemetry、自研 每次工具调用一个 span

Loop 伪代码:

state = init(goal, context)
for step in range(max_steps):
    context = assemble(state)
    action = llm(context, tools)
    if action.is_final:
        return action.answer
    result = execute(action, permissions)
    state = update(state, action, result)
    if should_stop(state):
        break

把报销案代进去走一遍(示意):第 0 步 assemble 拼出 system、工具定义和员工的问题,模型返回 search_policy(query="市内交通 超标");第 1 步拿到第 4.2 条,模型返回 query_invoices(date_range="上周", type="打车");第 2 步拿到 INV-102,模型接着调 query_tickets 查有没有重复报销;第 3 步返回空列表,update 用代码算出超标 38.5 元写进 state,模型这次返回 is_final,内容是结论加工单草稿,循环结束,等员工确认。max_steps 到这里只用掉 4 步。

终止条件有六种:任务完成、最大步数、超时、预算耗尽、人工中止、连续失败。

3. ReAct、Plan-Execute、Workflow 怎么选

范式 适合 不适合 风险
ReAct 探索型、工具反馈驱动 步骤必须严格固定的流程 死循环、上下文膨胀
Plan-Execute 多步、依赖清晰、可检查 高度动态、计划很快失效 计划质量、重规划
Workflow 合规、稳定、可审计 开放任务 不灵活
Multi-Agent 可并行、需隔离/审查 单 Agent + 多 Skill 就够 通信、状态、冲突

一个实用判断:能用 workflow 稳定解决的,不要上 Agent;能用单 Agent + 多 Skill 解决的,不要上多 Agent。

下面两张图可以逐步播放,左边一种边查边想,右边一种先写计划再执行。12 种结构的完整讲解和并排对比见 Agent 架构图解。

动画图:单体 ReAct 循环。启用 JavaScript 后可以逐步播放。

动画图:规划-执行。启用 JavaScript 后可以逐步播放。

报销案里四种都能找到位置:“确认后才建单”是 workflow 写死的;“查制度、查发票、判定”是一份短计划;“发票查不到怎么办”交给 ReAct 边查边想;多 Agent 用不上,除非任务变成审计全公司一个季度的差旅。

4. 记忆

短期记忆是当前会话和任务的工作状态:做到哪一步、工具返回了什么、还有什么没办。长期记忆是跨会话还会用到的东西:用户偏好、历史决策、领域事实。报销案里,“INV-102 超标 38.5 元”是短期的,任务结束就没用了;“这位员工常驻上海、默认成本中心 CC-2031(示意)”是长期的。

设计时要回答六个问题:

  • 存什么:只存下次还会用的信息,不存全量聊天记录。
  • 存哪里:结构化状态放 KV 或关系库,语义记忆放向量库,文件放对象存储。
  • 什么时候写:任务结束、用户明确要求、关键决策点。比如员工说“以后默认走 CC-2031”,这句话当场写入。
  • 怎么读:按用户、任务、时间检索,带权限过滤。
  • 怎么忘:TTL、重要性评分、冲突覆盖、用户删除。“这周在北京出差”这种条目设 7 天过期。
  • 怎么隔离:多用户、多租户之间的记忆不能串。

记忆管的是跨轮次、跨任务保留的信息,RAG 是检索后增强生成的方法,两者在实现上可以重叠(都可能用向量检索)。要分清的是来源、写入策略、作用域、生命周期和权限。

5. 多 Agent

常见结构有四种:

  • Supervisor:一个中心调度者分派任务、汇总结果。例:审计任务里主 Agent 把“销售部、研发部、行政部”三份差旅分给三个子 Agent。
  • Hierarchical:多层管理,适合规模大的分工。例:每个部门的子 Agent 下面再按月份拆。
  • P2P:Agent 之间直接对话,适合开放讨论。例:一个提方案、一个挑毛病,轮流发言。
  • 路由分发:按意图或能力把请求整个交给某个 Agent。例:入口识别出“报销问题”,转给财务助手。

生产上最常见的是 Supervisor,也就是主 Agent 加子 Agent:

动画图:主 Agent + 子 Agent。启用 JavaScript 后可以逐步播放。

层级、交接、群聊、黑板这几种结构的动画,以及它们和事件驱动工单的区别,见 Agent 架构图解。

角色可以拆成 planner、executor、critic、reviewer。拆开的收益是能并行、上下文隔离、各自专精、互相审查;代价是要通信、要同步状态、上下文会互相污染、结论会冲突、调试变难。

典型的失败有任务重复、信息丢失、互相推责任、死循环、结论冲突。对应的兜底是:每个子任务明确 owner、只保留一份共享状态、限制最大轮次、设仲裁者、最后由人收口。

6. 可靠性

  • 工具失败:重试(前提是幂等)、超时、降级、备用工具。例:发票接口返回 503,等 0.5 秒重试。
  • 参数错误:schema 校验、错误回喂、自动修复。例:模型把 date_range 填成“上礼拜”,校验报错“需要 YYYY-MM-DD 格式”,模型改成具体日期。
  • 死循环:步数上限、预算上限、重复动作检测、强制收口。例:同一个 query_invoices 参数调了 3 次,运行时直接打断。
  • 长任务:checkpoint、断点续跑、幂等键。例:建单带幂等键,服务重启后不会多出第二张单。
  • 人工:高风险操作审批、超时恢复。例:员工两小时没确认,任务挂起而不是自动提交。

7. 60 秒回答模板

我把 Agent 定义为 LLM + 工具 + 记忆 + 规划/循环,在环境里感知、决策、行动,再根据反馈继续。架构上分模型、指令、工具、记忆、规划器、运行时和护栏;Loop 是观察、规划、调工具、看结果、决定继续还是结束,必须有终止条件。范式上 ReAct 适合探索,Plan-Execute 适合多步可检查的任务,workflow 适合稳定合规的场景。记忆分短期状态和长期记忆,要和 RAG 区分开。多 Agent 我不会默认用,先看任务是否真的需要并行或隔离。最后靠重试、超时、checkpoint 和 HITL 兜底。

如果面试官让你举例,直接用报销案:“查制度、查发票、判定、确认、建单是 workflow 骨架,缺什么信息、怎么追问由模型决定;建单前必须人确认,建单带幂等键,状态存在模型之外。”

8. 常见追问(带答法)

Agent 和 workflow 的边界在哪?什么场景不该用 Agent?

简答:看下一步由谁决定。开发时就写死的是 workflow,运行时由模型看了观测再决定的是 Agent。步骤可以枚举、要求强审计、错一次代价很高、对延迟极敏感的场景,不该用 Agent。

细说:我一般用三个问题来判断。第一,步骤能不能在开发时列举出来?能,就用 workflow。第二,下一步是否依赖运行时才知道的信息(工具结果、用户回复),而且分支多到写不完?是,再考虑 Agent。第三,错一次代价多大?代价越大,越要把关键路径写死,只把开放的子问题交给模型。

举个对比。“每月 5 号生成部门差旅汇总表”每一步都确定,写成定时 workflow 就行,用 Agent 只会更贵更不稳定。“员工随口描述一笔打车,问能不能报”就不一样:他可能没说日期,可能用了个人账户,可能同一行程已经报过一次,这些分支写不完,适合让模型处理。

不该用 Agent 的典型场景还有:固定审批流、对账;毫秒级的在线调用;监管要求每一步都能预先审批的流程;输入输出完全结构化、规则就能判定的任务。

生产上常见的是混合。一种是“workflow 骨架 + Agent 节点”,关键路径和确认点写死,开放子问题交给 Agent;另一种是“Agent 外壳 + workflow 工具”,Agent 决定什么时候调用一个确定的子流程,比如把“生成工单草稿”封装成一个固定流程的工具。

容易踩的坑:Agent 的灵活性是拿不可预测、难测试、更贵换来的;可要是把所有长尾都写成分支,workflow 会膨胀到没人维护得动。边界最好用数据来定:先看 workflow 能覆盖多大比例的请求,剩下的长尾交给 Agent,再用评测确认确实有收益。

可能的追问:“怎么从 workflow 演进到 Agent?”分三步走。先在某个节点引入 LLM 做分类或抽取;再让模型在少数预设分支里选;最后才在受限工具集上放开循环。每走一步都跑评测,确认没有变差。

放到报销案例里:“建单前必须确认”“必须先查重复报销”是写死的约束;“发票缺失时怎么追问”“超标事由要不要补材料”可以交给模型。

工具调用失败、超时或返回脏数据,怎么重试和降级?

简答:先分类,再处理。瞬时故障退避重试,参数错误回喂让模型改,权限问题直接停,超时当作“结果未知”先对账,脏数据先校验再用。这些都不行就降级或转人工。写操作只在幂等的前提下重试。

细说:

错误类型 例子 处理
瞬时故障 网络抖动、429 限流、503 指数退避重试,尊重 Retry-After
参数错误 400、schema 校验失败 结构化错误回喂,让模型改参,限 1 到 2 次
权限 401 / 403 不重试;提示需要授权或转人工
业务结果 “该日期无发票” 这不是故障,是一条观测,交给模型决定下一步(如追问)
超时 写操作无响应 未知态:用幂等键查询结果后再决定
脏数据 字段缺失、类型错、金额为负、文本里夹带指令 schema + 业务规则校验,标注为不可信数据

脏数据要在进入上下文之前处理。先归一化:类型、单位、长度截断。再对关键字段做合理性校验:金额为负、日期在未来、发票抬头不是本公司。比如 query_invoices 返回 {"id": "INV-102", "amount": "-118.5"},金额是字符串还是负数,校验直接拦下,运行时重查一次;还不对,就标成“数据可疑”,让模型如实告诉员工,不能默默拿去建单。工具结果里出现“请忽略之前的指令”这类文本,也只当数据,不当指令,见 安全边界。

容易踩的坑:把所有错误都当瞬时故障重试,会放大故障、浪费预算;把业务结果当系统故障,会错过正确的下一步(“该日期无发票”应该引出追问,不该触发重试)。模型层和调用层各自重试,次数会相乘:调用层每次最多试 3 次,模型层又让它整体重来 3 次,同一个接口最多会被打 3 × 3 = 9 次。重试预算要统一管。

可能的追问:“模型选错工具、填错参数怎么兜底?”运行时用 schema 和权限校验拦住,把错误回喂。高频错误要回头改工具描述和参数示例,再加进回归评测,光靠重试解决不了。

长任务怎么断点续跑?状态存在哪?

简答:把任务建模成持久化的状态机。每个关键步骤前后写 checkpoint,内容包括目标、计划、步骤状态、关键结果、幂等键、待确认动作,存在数据库里,不放进程内存。恢复时加载最近一次 checkpoint,已完成的步骤直接复用结果,没完成的带着原来的幂等键继续。

细说:状态机从 pending 开始,进入 running,中间可能停在 waiting_user / waiting_confirm,最后落到 done / failed / cancelled 之一。checkpoint 里放任务元数据、计划和每步状态、每步输入输出的摘要(大结果放对象存储,只存句柄)、外部副作用记录(幂等键、外部单号)、预算消耗和消息摘要。存储用关系库或 KV,以 task_id 为主键,带版本号做乐观锁,防止两个 worker 同时恢复同一个任务。

恢复流程是:先发现中断(worker 心跳超时),再抢占任务锁、加载 checkpoint,对处于 running 的步骤先对账,看它是否已经产生了副作用,然后继续。举个例子:worker 在“建单”这步发出请求后崩了,checkpoint 里这步是 running,幂等键是 t-123:S6:INV-102。新 worker 接手后先用这个键去查,查到工单 R-55821 已经存在,就把这步标成 done,直接回复员工,不会再建第二张。

容易踩的坑:checkpoint 写得太密影响性能,太稀则恢复时要重做很多步;有外部副作用的步骤,前后都必须写。只存消息历史、不存结构化状态,恢复后模型得“重新理解”局面,很容易走出另一条路。

可能的追问:“状态在数据库里,上下文怎么恢复?”从结构化状态重新组装上下文:状态摘要加最近几轮消息,而不是回放完整历史,这也是上下文工程的一部分。幂等与“超时未知态”的细节见下文逐题作答和 可靠执行。

扩展:把一次循环写成状态更新

每一轮循环做三件事。模型看着上下文挑一个动作,工具执行这个动作并返回观测,运行时把动作和观测记进任务状态;下一轮再从新的状态重新拼上下文。写成式子是(教学抽象):

at∼πθ(⋅∣ct),ot+1=Tool⁡(at),st+1=U(st,at,ot+1)a_t\sim\pi_\theta(\cdot\mid c_t),\qquad o_{t+1}=\operatorname{Tool}(a_t),\qquad s_{t+1}=U(s_t,a_t,o_{t+1})

逐个符号看:

符号 读法与含义 报销案里是什么
tt 第几步,从 0 开始数 0、1、2、3
sts_t 第 t 步开始时的持久任务状态,存在数据库里 前文那份任务状态 JSON
ctc_t 由 sts_t 组装出的上下文,就是这次发给模型的消息列表。式子里省略了组装这一步,可以理解为 ct=assemble⁡(st)c_t=\operatorname{assemble}(s_t) system + 工具定义 + 状态摘要 + 最近几轮对话
πθ\pi_\theta 策略,这里就是模型本身;θ\theta 是模型参数 你调用的那个 LLM
πθ(⋅∣ct)\pi_\theta(\cdot\mid c_t) 竖线读作“给定”:在给定上下文 ctc_t 的条件下,各个候选动作的概率分布。点号 ⋅\cdot 是“任意一个动作”的占位 先查制度 0.7,先查发票 0.25,直接回答 0.05(示意)
at∼…a_t\sim\ldots 波浪号读作“服从、从中抽取”:动作是从上面的分布里采样出来的 search_policy(query="市内交通 超标")
ot+1=Tool⁡(at)o_{t+1}=\operatorname{Tool}(a_t) 运行时执行动作,得到观测 ot+1o_{t+1}。下标是 t+1,因为观测要到下一步才被模型看到 第 4.2 条的条款片段和出处
UU 状态更新函数,是运行时代码,不是模型 把条款出处写进 facts.policy_ref,把步数加一

手算一遍报销案的前三步(数值示意):

  1. t=0t=0:s0s_0 只有目标,facts 为空。模型给出的分布里“先查制度”占 0.7,采样得到 a0=a_0= search_policy。o1o_1 是第 4.2 条:P6 在上海市内交通标准 80 元,超标需写事由并经负责人审批。UU 写入 s1s_1:standard = 80,policy_ref = 第 4.2 条。
  2. t=1t=1:模型看到条款,下一步选 a1=a_1= query_invoices(date_range="上周", type="打车")。o2o_2 返回 INV-102,金额 118.5 元。UU 写入 s2s_2:invoice = INV-102,并且由代码算出 over_amount = 118.5 − 80 = 38.5。
  3. t=2t=2:模型选 a2=a_2= query_tickets 查重,o3o_3 为空列表,s3s_3 记下“无重复报销”。下一步模型输出结论和草稿,等员工确认。

第 1 步里那个 0.7 也可以算一下:如果采样温度不为 0,同样的 c0c_0 跑 100 次,大约 70 次先查制度、25 次先查发票、5 次直接回答(示意)。所以排查线上问题要看当时记录下来的 ata_t 和 ot+1o_{t+1},拿同样的输入重跑一次不一定能复现。

把 ss 和 cc 分开写,是因为上下文会被裁剪、压缩、重新排序,状态不能丢。38.5 这个数由 UU 用代码算出来、存在 ss 里,后面无论上下文怎么压缩它都在。这个式子也有省略的地方:真实系统在执行 ata_t 之前还要检查权限、schema 和预算;模型给出“完成”只是 ata_t 的一种取值,是候选结论,要由验收器去查外部状态(工单是否真的存在)后才能让 UU 把任务标成 done。

ReAct 原论文 讨论的就是推理和动作交替进行。工程日志里保留动作、观测和可检验的决策摘要就够了,不要把模型的隐藏推理当成必须读取的依赖。固定流程和自主决策怎么取舍,可以参考 Building effective agents 里的几种工作流模式。

多 Agent 的增益要和同预算的单 Agent 比;并行的子任务也要讲清交付格式、共享状态归谁、冲突怎么仲裁、什么时候停。超时后动作到底成功没有、恢复后会不会重复写入,见 可靠执行:状态、幂等与恢复。

逐题作答

下面按知识图谱里本主干的分支逐题回答。括号里是题目在面经中的出现热度;上文“常见追问”已经答过的高频题不再重复。

定义与边界

Agent 是什么?和 LLM 有什么区别?(121/58)

简答:LLM 是一个无状态的函数,给它上下文,它往后生成 token。Agent 是把 LLM 放进一个循环,循环里有目标、工具、状态和终止条件;模型的输出被变成对外部系统的动作,动作结果再回来影响下一次决策。

细说:先看同一个问题在两者手里的样子。直接问 LLM“我上周打车 118.5 元能报吗”,它能讲讲一般的报销规矩,但它不知道你是谁、标准是多少、发票在不在系统里,也没法帮你建单。放进 Agent 里,同一个模型会先查第 4.2 条,再查你名下的发票,算出超标 38.5 元,给出工单草稿等你确认。差别在模型外面那一圈。

维度 LLM Agent
输入输出 文本进、文本出(含结构化输出) 目标进,外部状态变化 + 结论出
状态 单次调用无状态,“记忆”只是上下文里的内容 运行时持有任务状态,跨步、跨轮持久化
对外影响 不直接改变外部系统 通过工具读写外部系统
步数 一次生成 多轮“决策、执行、观察”
失败处理 交给调用方 内建重试、重规划、收口、转人工
正确性 看文本质量 看任务是否真的完成(外部状态可验证)

还有一点常被说错:LLM 自己不“调用”任何东西。它只输出一段结构化意图,比如 {"name": "query_invoices", "arguments": {"date_range": "上周"}};鉴权、执行、把结果填回去的是运行时。所以一个 Agent 好不好用,取决于模型的决策质量和运行时的工程质量,两者缺一样都不行。

容易踩的坑:第一种错误答法是“Agent = LLM + 工具”,漏了循环和状态,单次 function call 算不上 Agent。第二种是把 Agent 说成“更聪明的模型”,可同一个模型放进不同的运行时,可靠性能差很多。最好主动提代价:延迟和 token 随步数增长,错误会在步骤之间累积。

可能的追问:“模型已经能输出工具调用了,Agent 还缺什么?”缺执行器、权限、状态持久化、终止和验收。没有这些,模型说“已建单”的时候,你没办法知道单子是不是真的建了。另一个常见追问是“Agent 的智能主要来自哪?”决策质量主要来自模型,可靠性主要来自运行时,两边都要讲。

放到报销案例里:LLM 能回答“超标一般怎么处理”;只有 Agent 能真的去查这位员工上周的发票 INV-102、对照第 4.2 条、提出建单。

Agent 和 workflow、ChatBot、RAG、Function Calling 怎么区分?(52)

简答:这五个概念不在同一层。ChatBot 是交互形态,RAG 是供给知识的方法,Function Calling 是模型输出动作的接口机制,workflow 和 Agent 是两种控制流。区分后两者的硬标准是“下一步由谁决定”。

细说:

概念 所在层 下一步谁定 是否改外部状态 典型形态
ChatBot 交互形态 用户 通常不改 问答、客服 FAQ
RAG 知识供给 固定流水线 不改 检索、拼上下文、生成
Function Calling 模型接口 模型提议、代码执行 可能改 一次结构化动作
Workflow 控制流 开发者预先编排 可能改 DAG、状态机、条件分支
Agent 控制流 模型在运行时决定 通常会改 循环 + 工具 + 状态

它们经常叠在一起用。报销助手就是例子:对员工来说它是一个聊天窗口(ChatBot 形态);里面的 Agent 用 Function Calling 作为动作接口;search_policy 这个工具背后是一套 RAG;而“确认后才建单”这一段又是写死的 workflow。

容易踩的坑:别说“workflow 里没有 LLM”,节点里完全可以用 LLM 做分类、抽取。也别说“有循环就是 Agent”,固定重试 3 次的循环仍然是 workflow。还有一种中间形态:模型只在预设的几个分支里选一个,这是路由型 workflow,自主性有限。

可能的追问:“Agentic RAG 和普通 RAG 差在哪?”普通 RAG 检索一次就生成。Agentic RAG 由模型决定要不要检索、检索什么、证据够不够、要不要改写查询再查一次。比如第一次用“打车超标”只搜到第 4.2 条,模型发现条款里提到“见附件 3 审批权限”,再发起第二次检索。代价是更慢、更贵,还需要终止条件。

Agent 的核心要素有哪些?缺一个会怎样?

简答:模型、工具、记忆/状态、规划、循环、环境反馈,这六样让 Agent 能动;再加一个常被忽略的边界,让它可控。讲的时候按“缺了会怎样”逐个说明。

细说:

要素 作用 缺了会怎样(报销案)
模型 理解目标、选择动作 只能写死规则,员工换个说法就识别不了
工具 动作空间,触达外部系统 只能说不能做,退化为 ChatBot,查不到 INV-102
记忆/状态 记住做到哪、已知什么 每步从头来,第 4.2 条查了三遍
规划 拆目标、遇阻改道 没查重复报销就去建单
循环 允许多次决策 只有一步,等于一次 function call
环境反馈 工具结果、错误、用户回复回到上下文 发票接口报错了模型也不知道,照样说“已查到”
边界 权限、预算、终止、人工确认点 能动但不可控,可能没经员工确认就提交

容易踩的坑:这些要素不必每个都做得很重。规划可以只是模型隐式地选下一步(ReAct),记忆可以只是会话内的状态。按任务复杂度配置就好,别为了架构图好看把每个要素都拆成独立服务。

可能的追问:“环境反馈和记忆有什么区别?”反馈是这一步刚发生的观测;记忆是筛选过、可能跨步跨会话保留的信息。反馈不一定进记忆。比如 query_invoices 返回了 20 个字段,用完之后只留“INV-102,118.5 元,上海市内”这一行摘要就够了。

Agent 的自主性从哪来?哪些环节必须由人控制?

简答:自主性来自“模型在运行时根据观测选下一步”这件事,它能做到多大,由动作空间和权限决定。目标和验收标准、权限授予、不可逆或对外的写操作、证据冲突时的判断,必须留给人或确定性规则。

细说:模型能自己做决定,靠三样东西。一是训练:预训练加上指令微调、工具调用训练,让模型会读工具描述、选工具、填参数。二是上下文:工具描述和观测给了它决策的依据。三是循环:决策可以根据新信息修正。

下面是一个实用的自主性分级(示意):

级别 允许做什么 例子
L0 建议 只给建议,人执行 写一段说明给用户
L1 受限选择 在预设分支或工具子集里选 路由到某个流程
L2 自主读 自由调用只读工具 查制度、查发票
L3 自主写(可撤回) 草稿、预留、可取消的写 生成报销单草稿
L4 不可逆写 付款、删除、对外发送 原则上加确认

容易踩的坑:确认点太多,Agent 就变成每一步都弹窗,人会不看内容直接点同意,确认也就失去了意义。应该按风险分级,只在影响大的节点确认,确认界面上要给出依据和影响。

可能的追问:“模型说自己很确定,能不能跳过确认?”不能。模型对自己的置信度判断不可靠。风险等级应该由代码根据动作类型和参数(金额、对象、能否撤回)来判定,比如“写操作且金额大于 0 就要确认”。

放到报销案例里:查发票是 L2;建报销单在 L3 到 L4 之间,要先展示“118.5 元,超标 38.5 元,依据第 4.2 条”,员工确认后才执行;“超标批不批”是审批人的事,Agent 不能代替。详见 人在回路。

架构与 Loop

一个完整的 Agent 架构包含哪些组件?各自的边界在哪?(92)

简答:按“决策、执行、治理”三层讲。模型和指令负责决策,工具、状态和运行时负责执行,护栏和可观测负责治理。整套架构遵守一条原则:模型只提议,运行时做裁决。

细说(组件清单见上文第 2 节,这里讲数据怎么流):

[用户 / 事件]
   ↓
入口层:鉴权、会话、限流、意图路由
   ↓
决策层:Instructions + 上下文组装器 + LLM(+ Planner)
   ↓ 结构化动作提议
执行层:策略检查 → 工具执行器 → 结果归一化
   ↓ 观测
状态层:任务状态 / 短期记忆 / 长期记忆 / Checkpoint
   ↓
治理层:Guardrails、HITL、Trace / Metrics、评测

跟着一个建单动作走一遍:员工点了“确认”,入口层核对登录态,拿到 employee_id;决策层把状态摘要和确认消息拼进上下文,模型提出 create_reimbursement_ticket;执行层先查策略(写操作,已确认,金额 118.5 元和确认时一致),再执行,返回工单号 R-55821,归一化时只保留单号和状态;状态层把这一步标为 done 并写 checkpoint;治理层记下这次调用的 span 和确认记录。

各层之间的边界:

  • 模型只产出提议,不直接持有凭证;
  • 上下文组装器是往模型里放东西的唯一入口,便于审计和裁剪;
  • 工具执行前必过权限与 schema 校验,执行后结果要归一化(截断、脱敏、标注来源);
  • 任务状态由运行时写,模型不能直接把任务标成“已完成”;
  • 每一步都有 trace span。

容易踩的坑:常见的失分是只列组件、不讲数据流。另一个坑是把“工具可插拔”理解成“多注册几个函数”。要做到可插拔,需要统一的工具契约(名称、描述、参数 schema、权限标签、副作用类型、超时)和一个注册表,见 工具/协议。

可能的追问:“Guardrails 放在哪?”至少三处。输入侧检测注入和越权意图;动作侧在执行前做策略检查;输出侧检查格式、敏感信息和事实。只放在输出侧的话,副作用已经发生了,拦不回来:单都建了,再检查回复文本也没用。

Agent Loop 怎么运转?一次交互在底层如何与模型、工具打交道?(52)

简答:用户眼里的“一次交互”,底层通常是好几次模型调用。运行时组装上下文、附上工具定义去调模型;模型返回文本,或者返回工具调用请求;运行时校验并执行,把结果作为 tool 消息追加进去,再调一次模型。如此往复,直到模型给出最终回答或者触发终止条件。

细说(报销案的一次往返,示意):

1  user: 上周打车超标还能报吗?能的话帮我提单
2  runtime → LLM: [system, 工具定义(search_policy, query_invoices,
                   query_tickets, create_reimbursement_ticket), user]
3  LLM → tool_call: search_policy(query="市内交通 超标 报销")
4  runtime: 校验参数 → 执行 → 返回条款片段 + 出处
5  LLM → tool_calls(并行):
        query_invoices(date_range=上周, type=打车)
        query_tickets(date_range=上周, type=交通)
6  runtime: employee_id 从登录态注入,不用模型填的值 → 并发执行
7  LLM → assistant: 结论 + 依据 + 工单草稿,请你确认事由   ← 本轮结束
8  user: 事由是客户临时改了会面地点,确认
9  LLM → tool_call: create_reimbursement_ticket(...)
10 runtime: 风险检查 → 生成幂等键 → 执行 → 返回工单号 R-55821
11 LLM → final: 已提单,工单号 R-55821,依据第 4.2 条

在员工看来这是两轮对话,底层其实调了 5 次模型(第 3、5、7、9、11 行各一次)。

第 5、6 行在消息里大致是这样的(字段名因厂商而异,示意):

[{"role": "assistant", "tool_calls": [
   {"id": "call_1", "name": "query_invoices",
    "arguments": {"date_range": "2026-10-26~2026-11-01", "type": "taxi"}},
   {"id": "call_2", "name": "query_tickets",
    "arguments": {"date_range": "2026-10-26~2026-11-01", "type": "transport"}}]},
 {"role": "tool", "tool_call_id": "call_1",
  "content": "[{\"id\": \"INV-102\", \"amount\": 118.5, \"city\": \"上海\"}]"},
 {"role": "tool", "tool_call_id": "call_2", "content": "[]"}]

底层有几个要点:

  • 常见的 Chat 类模型接口是无状态的,每次调用都要把需要的消息重新发一遍,对话历史由运行时维护。有些接口提供服务端会话(传上一次响应的 id 就能接上),但任务状态仍然应该存在你自己的系统里。
  • 每个工具调用请求带一个 id,结果消息回填同一个 id,模型才能对上号。上面 call_1 对应发票结果,call_2 对应空列表。
  • 模型可以一次提出多个互不依赖的调用,运行时并发执行。
  • 流式输出时,运行时要边收边判断这一段是普通文本,还是工具调用的片段。

容易踩的坑:常见错答是“模型直接调用了接口”。另一个坑是把工具结果原样塞回上下文:发票接口一次返回 20 个字段,几轮下来上下文就爆了。结果要裁剪、摘要,或者只回传句柄。

可能的追问:“工具调用发生在调模型之前还是之后?”由模型决定的调用,在模型返回之后执行。但“每次都要先查用户档案”这类确定动作,可以由代码在调模型之前预取,那一部分属于 workflow。

消息和状态怎么组织?system、user、assistant、tool 结果和任务状态各放哪?(16)

简答:消息是拼给模型看的对话记录,状态是运行时持有的事实记录。每一轮的上下文都从状态重新组装,别把消息列表当成唯一的状态。

细说:

内容 位置 生命周期 说明
角色、规则、输出格式 system 稳定 放在前缀,利于缓存
用户输入 user 每轮 外部输入,不可信
模型回复、工具调用 assistant 每步 带 tool_call id
工具结果 tool 每步 不可信数据,标注来源
任务状态 运行时数据库,摘要进上下文 任务级 真相来源
长期记忆 外部存储,检索后注入 跨会话 带来源与时间

任务状态示意:

{"task_id": "t-123",
 "goal": "判断打车超标能否报销,可以则提单",
 "plan": [{"step": "查制度", "status": "done"},
          {"step": "查发票与已有报销单", "status": "done"},
          {"step": "确认事由与金额", "status": "waiting_user"},
          {"step": "建单", "status": "pending"}],
 "facts": {"policy_ref": "《差旅费管理办法》2026 版第 4.2 条",
           "invoice": "INV-102", "amount": 118.5,
           "standard": 80, "over_amount": 38.5},
 "pending_action": null,
 "budget": {"steps_used": 4, "max_steps": 12}}

这份状态进上下文时,不会原样塞进去,而是变成一段摘要,比如:“进度 2/4;已知 INV-102 金额 118.5 元,标准 80 元,超标 38.5 元,依据第 4.2 条;正在等员工补事由。”

容易踩的坑:一是把工具结果拼进 system,等于把不可信数据提到了指令的级别,注入就从这里进来。二是只靠消息历史当状态,压缩时一删,就丢了“做到哪”。三是消息里说“完成”、状态里是“失败”时,以状态为准。

可能的追问:“每轮重写上下文会不会破坏缓存?”会影响前缀缓存的命中,所以稳定的内容(system、工具定义)放前面,变动的内容放后面,见 上下文工程。

终止条件怎么设计?最大步数、超时、预算分别防什么?

简答:终止分两类,正常完成和强制收口。正常完成要有可验证的判据;强制收口靠步数、墙钟时间、token 或费用预算、连续失败次数和人工中止。收口时要交代做到哪了、为什么停、下一步怎么办,不能静默失败。

细说:

条件 防什么 实现
完成判据 模型自称完成但没做完 校验外部状态,如工单是否真实存在
最大步数 死循环、路径震荡 计数器,按任务类型配置
墙钟超时 卡在慢工具上 整体 deadline + 单工具 timeout
token / 费用预算 成本失控 每步累计,接近上限提示收口
连续失败 反复撞同一个错误 同类错误 N 次即停
人工中止 / 拒绝 用户不想继续 每步检查中止信号

把这几条合起来,就是“全部满足才继续,任何一条不满足就停”。写成教学式子:

continuet=1 ⁣[ t<Tmax⁡ ∧ ∑i≤tci<B ∧ τt<D ∧ ¬ done(st) ]\text{continue}_t=\mathbf{1}\!\left[\,t<T_{\max}\ \wedge\ \sum_{i\le t}c_i<B\ \wedge\ \tau_t<D\ \wedge\ \neg\,\mathrm{done}(s_t)\,\right]

逐个符号看:

  • tt:已经走完的步数。Tmax⁡T_{\max}:最大步数。
  • cic_i:第 i 步花掉的费用(元,或换算成 token)。∑i≤tci\sum_{i\le t}c_i 读作“对所有不超过 t 的 i 求和”,就是到目前为止的累计花费。BB:预算。
  • τt\tau_t:任务开始到现在过去的时间(秒)。DD:截止时长。
  • done(st)\mathrm{done}(s_t):验收器看了状态 sts_t 和外部系统之后给出的“是否已完成”,值为真或假。¬\neg 读作“非”,¬ done\neg\,\mathrm{done} 就是“还没完成”。
  • ∧\wedge 读作“并且”,四个条件必须同时成立。
  • 1[⋅]\mathbf{1}[\cdot] 是指示函数:方括号里的条件成立记 1(继续),不成立记 0(停)。

手算一个例子(示意):设 Tmax⁡=12T_{\max}=12,B=0.50B=0.50 元,D=120D=120 秒。前 7 步的费用依次是 0.06、0.08、0.05、0.09、0.07、0.10、0.08 元。

  • 第 5 步走完:t=5<12t=5<12 成立;累计 0.06+0.08+0.05+0.09+0.07=0.35<0.500.06+0.08+0.05+0.09+0.07=0.35<0.50 成立;已用 48 秒 <120<120 成立;任务还没完成,¬ done\neg\,\mathrm{done} 成立。四条都成立,结果为 1,继续。
  • 第 7 步走完:累计变成 0.35+0.10+0.08=0.530.35+0.10+0.08=0.53,0.53<0.500.53<0.50 不成立,结果为 0,停下收口。其他三条成立也没用。

这个式子有一个边界要注意:它是“走完一步再检查”,所以上面的例子实际花了 0.53 元,超了预算一步。要严格不超,就在执行下一步之前用预估费用判断,即要求 ∑i≤tci+c^t+1<B\sum_{i\le t}c_i+\hat c_{t+1}<B,其中 c^t+1\hat c_{t+1} 是对下一步费用的估计。另外 done\mathrm{done} 必须由验收器给出,如果直接拿模型说的“完成了”来填,这个式子就没有意义了。

容易踩的坑:max_steps 太小会截断正常的长任务,太大则失败时白烧钱。更好的做法是“软上限 + 硬上限”:比如软上限 8 步、硬上限 12 步,到第 8 步时在上下文里加一句“请收敛,给出当前结论”,到第 12 步直接停。强制停止不能留下半完成的写操作。

可能的追问:“达到上限时返回什么?”返回部分结果、已完成的步骤、没完成的原因和可恢复的 task_id,让用户或人工接手。例如:“已查到发票 INV-102 和第 4.2 条,判定可报但需补事由;还没建单。任务 t-123 已保存,补充事由后可继续。”

规划与执行范式

ReAct 是什么?解决了什么,又有什么问题?(65/47)

简答:ReAct 让模型交替做三件事:推理(Thought)、动作(Action)、看结果(Observation),用工具返回的真实观测来校正推理。它适合需要边查边想的探索型任务;问题是只顾眼前一步、容易绕圈、上下文随步数变长。

细说:原论文(Yao et al., 2022)的格式是 Thought、Action、Observation 三步循环。用报销案里“发票查不到”这个分支写一段轨迹(示意,假设这位员工的发票被登记成了网约车类型):

Thought 1: 要判断能不能报,先确认上周有没有打车发票。
Action 1: query_invoices(date_range="2026-10-26~2026-11-01", type="taxi")
Observation 1: []
Thought 2: 一张都没有。员工说的“打车”也可能是网约车,发票系统里网约车和
           出租车是两个类型,换成 ride_hailing 再查一次。
Action 2: query_invoices(date_range="2026-10-26~2026-11-01", type="ride_hailing")
Observation 2: [{"id": "INV-102", "amount": 118.5, "date": "2026-10-29"}]
Thought 3: 找到了,10 月 29 日,118.5 元。下一步查有没有重复报销。
Action 3: query_tickets(invoice_ids=["INV-102"])
Observation 3: []
Thought 4: 没有重复报销。对照第 4.2 条:标准 80,超标 38.5,需要事由。
Action 4: finish("可报,超标 38.5 元,请补充事由后确认建单")

ReAct 的用处体现在第 2 步:观测是空列表,模型据此换了一个假设再查,没有直接回答“你没有发票”。如果是先写好完整计划再执行,计划里多半只会写 type="taxi",查空了就只能等重规划。

现在主流模型原生支持工具调用,Thought 往往变成模型内部的推理,或者一两句简短说明;Action 是结构化的 tool_call,Observation 是 tool 消息。形式和原论文不同,推理和动作仍然是交替进行的。

  • 优点:观测接地,比纯推理少一些幻觉;每步都看新信息,能随时改道;实现简单,一个循环就够。
  • 缺点:每步调一次模型,延迟和成本高;没有全局计划,长任务容易偏题;可能反复调用同一个工具;上下文线性增长,上面 4 步已经积了 4 段观测。

容易踩的坑:一个过时的认知是“ReAct 必须让模型写出 Thought 文本”。对原生支持工具调用的模型,强制输出长篇推理不一定更好;审计也应该看动作和观测,而不是依赖模型写出来的推理文字。另一个坑是把 ReAct 当成所有任务的默认解。

可能的追问:“ReAct 和 CoT 的区别?”CoT 只在模型内部推理,没有外部观测;上面那段轨迹如果去掉 Action 和 Observation,模型只能猜“可能没发票”。ReAct 把推理和外部动作交织在一起,信息随观测更新。再问“ReAct 死循环怎么办?”答重复动作检测加步数上限,再换策略或收口,见本节后面“死循环”那一题。

Plan-and-Execute 怎么工作?和 ReAct 怎么选?(38)

简答:先由规划器产出一份步骤列表(可以带依赖和完成判据),再由执行器逐步或并行执行,碰到意外才回到规划器重规划。它用前置思考换来更少的大模型调用和更好的可审查性,代价是计划可能在执行中过时。

细说:

plan = planner(goal, context)          # 步骤、依赖、完成判据
while plan.has_pending():
    step = plan.next_ready()
    result = executor(step)             # 一次工具调用,或一个小 ReAct
    plan.mark(step, result)
    if result.unexpected:
        plan = replanner(goal, plan, result)
return synthesize(plan)

报销案里规划器产出的计划大致是这样(示意):

{"goal": "判断上周打车超标能否报销,可以则提单",
 "steps": [
  {"id": "S1", "do": "search_policy", "args": {"query": "市内交通 超标"},
   "done_when": "拿到条款编号和标准金额"},
  {"id": "S2", "do": "query_invoices", "args": {"date_range": "上周", "type": "taxi"},
   "done_when": "拿到发票列表"},
  {"id": "S3", "do": "query_tickets", "depends_on": ["S2"],
   "done_when": "确认这些发票没被报过"},
  {"id": "S4", "do": "judge", "depends_on": ["S1", "S2", "S3"],
   "done_when": "得出 可报 / 需补事由 / 不可报"},
  {"id": "S5", "do": "ask_user_confirm", "depends_on": ["S4"]},
  {"id": "S6", "do": "create_reimbursement_ticket", "depends_on": ["S5"]}]}

S1 和 S2 没有依赖,可以并行;S3 要等 S2 的发票号。假设 S2 返回空列表,就触发 result.unexpected,重规划器只改 S2 之后的部分,比如插一步“追问员工是否用个人账户打车”,S1 的结果原样保留。

维度 ReAct Plan-and-Execute
大模型调用 每步都调 规划一次,执行可用小模型或代码
适应性 高 依赖重规划
可审查 事后看轨迹 事前可审计划,甚至给人确认
并行 难 依赖图明确后可并行
适合 信息不确定、探索型 步骤可预见、依赖清晰

相关变体:ReWOO 先写带占位变量的计划(比如“#E2 = query_invoices(...),#E3 = query_tickets(#E2)”),减少中间观测反复回喂给大模型;LLMCompiler 把计划编译成可并行执行的 DAG。

容易踩的坑:计划写得越细,越容易过时。重规划的触发条件要写明白:步骤失败、观测和假设不符、用户补充了新信息。不然要么从来不重规划,要么每步都重规划,后者就成了更贵的 ReAct。实践中常见的是混合:外层 Plan-and-Execute,单个步骤内部用 ReAct。上面 S2 内部就可以跑前一题那段“换个发票类型再查”的小循环。

可能的追问:“规划是框架做还是模型做?”计划的内容由模型生成;计划的数据结构、状态推进、依赖调度、什么时候触发重规划,由运行时负责。能写死的步骤直接写进 workflow,不必让模型每次重新规划。

放到报销案例里:报销案的步骤基本可预见(查制度、查发票与已有报销单、判定、确认、建单),一份轻量计划就够;“发票没查到”时才重规划,比如追问是否用了个人账户打车。更多见 规划与状态。

Reflection 反思机制是什么?什么时候真的有用?(18)

简答:Reflection 是让 Agent 评估自己的输出或轨迹,再据此修正:重试、改计划、或者记下一条经验。它有用的前提是有外部可检验的反馈信号;只凭模型自我感觉的反思收益有限,甚至会把对的改成错的。

细说:常见有三种形态。

  1. 输出自修正(如 Self-Refine):先生成,再自评,再改写,适合格式、完整性、风格。例:工单草稿写完,自评发现“事由”字段空着,补上。
  2. 轨迹反思(如 Reflexion):任务失败后,把“哪里错了”总结成一段文字,下一次尝试时带上。例:上次因为没查重复报销被财务退回,反思写成“建单前必须先调 query_tickets”,下次带进上下文。
  3. 外部 critic:单元测试、schema 校验、规则引擎,或者另一个按 rubric 打分的模型,给出反馈后 Agent 修正。

可靠程度的经验排序是:确定性检查器 > 有 rubric 的独立评审 > 同一模型无依据的自评。已有研究指出,在缺少外部反馈时,大模型对推理题的自我纠正并不可靠(Huang et al., 2023)。

容易踩的坑:每轮反思都是额外的调用,成本可能翻倍;轮数要设上限,通常一到两轮。写进长期记忆的“经验教训”需要先验证,不然会把错误经验固化下来,比如一次偶然的接口超时被总结成“发票系统周一不可用”。

可能的追问:“反思和重试有什么区别?”重试是同样的动作再来一次,对付瞬时故障;反思是换个策略再来,对付决策错误。对网络超时做反思是浪费,对参数错误盲目重试也是浪费。

放到报销案例里:建单前用规则校验比让模型“再想想”可靠得多。三条规则就够:金额 118.5 不超过发票合计 118.5;事由非空;引用的第 4.2 条在制度库里真实存在。任何一条不过,直接把具体哪条没过回喂给模型。

任务怎么拆分?多意图、依赖、粒度和重规划怎么处理?(25/20)

简答:拆分的目标是得到每步可执行、可验收、依赖明确的子任务。先识别意图和它们之间的条件关系,再画依赖图;粒度按“一个子任务对应一个可验收的输出”来定;执行中一旦观测和前提不符,就局部重规划。

细说:

  1. 多意图:一句话里可能有多个意图,而且彼此有条件关系。“能不能报”是查询,“帮我提单”是写操作,而且以前者为条件,必须显式拆开并记下这个条件。
  2. 依赖:画成 DAG,没有依赖的并行,有依赖的串行。
  3. 粒度:太粗,一步里混了查询和写入,失败时说不清是哪里错;太细,调用次数多、计划冗长。比如“查发票并建单”是太粗,“查发票的第 1 页”“查发票的第 2 页”是太细。
  4. 完成判据:每个子任务写清楚什么算做完。
  5. 重规划:子任务失败、结果推翻了前提、用户补充了新信息,只重排受影响的子图,已完成的部分保留。
T1 查制度(交通标准、超标规则)   ─┐
T2 查发票(上周、打车)           ─┼─→ T4 判定可报金额 → T5 用户确认 → T6 建单
T3 查已有报销单(防重复)         ─┘

局部重规划的例子:T5 时员工说“对了,那天还有一张 45 元的,也一起报”。受影响的是 T2 到 T6,T1 查到的制度条款不用重查。

容易踩的坑:让模型自由拆分,常见的问题是拆出重复的子任务,或者漏掉条件依赖(还没确认可报就去建单)。高频的业务场景里,把常见拆法写成模板或 Skill,模型只负责填充和处理例外。

可能的追问:“一句话里两个意图互相矛盾怎么办?”不要猜,先澄清,澄清本身就是计划里的一个子任务。比如“帮我把这单撤了,然后提交”,先问清楚员工要撤的是哪一单、提交的又是哪一单。

路由怎么做?规则、分类模型、LLM Router 各适合什么?(55/13)

简答:路由是把请求分给合适的处理路径:某个工具集、子 Agent、模型或者 workflow。生产上常用级联:规则兜住确定的,分类模型处理高频意图,LLM Router 处理长尾,置信度低的走澄清或兜底。

细说:

方式 优点 缺点 适合
规则 / 关键词 / 正则 快、确定、可审计、几乎零成本 覆盖差、维护难 格式明确的输入、合规必经路径
分类模型 快、便宜、稳定 要标注数据,新意图需重训 高频且类别稳定
LLM Router 零样本、理解复杂表述 慢、贵、有波动、可被注入 长尾、类别常变
级联混合 兼顾成本与覆盖 系统更复杂 多数生产系统
route = rules(q)
if route is None:
    route, p = classifier(q)
    if p < threshold: route = None
if route is None:
    route = llm_router(q, candidates)   # 结构化输出,枚举约束
if route is None or route.low_confidence:
    ask_clarify_or_fallback()

拿三句话过一遍这个级联(阈值 0.8,概率为示意):“查一下 R-55821 的进度”里有工单号格式,规则直接命中“工单查询”。“打车超标还能报吗”规则没命中,分类器给“报销咨询”0.93,过阈值。“上回出差那个事儿财务怎么还没给我回”分类器最高只有 0.52,交给 LLM Router,它在候选枚举里选了“工单查询”,接下来由工单查询流程追问是哪一单。

容易踩的坑:LLM Router 必须用枚举或结构化输出限制在候选集内,不能让它“发明”一条路由。路由错了会被下游放大,所以要监控每条路由的命中率和下游失败率。候选描述要尽量互斥,描述重叠是选错的主要来源。

可能的追问:“LLM 路由和规则路由怎么选?”看类别稳不稳定、错误代价多大、延迟预算多少。类别稳定而且错了代价高,用规则或分类器;类别开放、能容忍偶尔错,用 LLM。两者可以并存,规则优先。

放到报销案例里:报销问题被路由到“财务助手”工具集(制度检索、发票、工单);如果识别到“我要投诉审批人”,就转人工通道,不再让 Agent 处理。

记忆

短期记忆怎么设计?每一轮更新什么?(100/58/46)

简答:短期记忆是当前会话和任务的工作状态,不等于原始聊天记录。每轮要更新的是结构化的任务状态(目标、计划进度、已确认的事实、待确认的事项)和最近几轮原文,其余的用摘要或句柄代替。

细说:短期记忆通常包含四类内容。

  1. 对话窗口:最近 N 轮原文,保留语气和指代。员工说“就那张”,得靠上一轮原文才知道指的是 INV-102。
  2. 工作状态:目标、计划、步骤状态、关键的中间结论。
  3. 工具结果:原文放外部存储,上下文里只留关键字段、摘要或句柄。
  4. 约束与承诺:用户说过的限制(“别用我私人邮箱”),以及 Agent 承诺过的事(“确认后我再提交”)。
on_turn_end(turn):
    window.append(turn); window.trim(N)
    state.facts.update(extract_facts(turn))     # 结构化抽取,带来源
    state.plan.update(turn.tool_results)
    if tokens(window) > limit:
        summary = summarize(summary, window.pop_oldest())
    save(session_id, state, window, summary)    # 存外部,不靠进程内存

看一轮具体的更新。员工这一轮说:“事由是客户临时改了会面地点。哦对,金额是 118.5 不是 120。”这一轮结束后:

部分 更新前 更新后
facts.reason 空 “客户临时改了会面地点”(来源:用户第 3 轮)
facts.amount 120(来源:用户第 1 轮口述) 118.5(来源:用户第 3 轮更正,与 INV-102 一致)
plan 中“确认事由” waiting_user done
对话窗口 第 1 到 3 轮 第 2 到 4 轮,第 1 轮并入摘要

存储放会话级的 KV 或数据库(以 session_id / task_id 为键),不要放进程内存,否则多实例部署和重启都会丢。

容易踩的坑:一直把全部历史塞进上下文,成本会涨,长上下文中段的信息也容易被模型忽略。只用摘要会丢精确值(金额、单号),所以精确值必须进结构化状态。用户更正信息时要覆盖旧值,就像上面把 120 改成 118.5;如果让新旧两条同时留在窗口里,模型就得自己猜哪个对。

可能的追问:“短期和长期的边界?”看作用域和生命周期:只对当前任务有用的放短期,任务结束可以丢;对以后的任务也有用、而且用户允许保留的,才提升为长期。详见 记忆。

长期记忆存在哪?向量库、关系库、KV 怎么选?(33)

简答:按访问模式选。精确查找(偏好、配置)用 KV 或关系库,语义相似查找(过往经历、相似案例)用向量库,关系推理用图。生产上多是结构化存储为主、向量索引为辅的混合。

细说:

存储 存什么 查询方式 坑
KV 偏好、开关、最近状态 精确键 不支持语义查
关系库 有 schema 的事实、历史决策、审计 SQL 过滤、时间范围 要先设计 schema
向量库 非结构化经历、对话摘要、案例 相似度 + 元数据过滤 相似不等于相关,精确更新和删除难
图数据库 实体关系(人、项目、制度之间) 遍历 建图维护成本高
对象存储 文件、长文档 句柄 需配合索引

每条记忆至少带上这些字段:user_id / tenant_id、类型(事实 / 偏好 / 事件 / 经验)、内容、来源(哪次会话、哪个工具)、时间、是否经用户确认、过期时间。写出来是这样(示意):

{"memory_id": "m-8841", "tenant_id": "corp-a", "user_id": "u-1029",
 "type": "preference", "topic": "default_cost_center",
 "content": "报销默认走成本中心 CC-2031",
 "source": {"session": "s-77", "turn": 5, "by": "user"},
 "confirmed": true, "created_at": "2026-10-29", "expires_at": null}

这条有固定的主题键 default_cost_center,适合放关系库或 KV 精确查;如果是“上次出差客户临时改地点,最后补了说明才批下来”这种经历,才适合进向量库按语义召回。

容易踩的坑:只用向量库做长期记忆,会碰到几个问题:旧事实没法精确覆盖,删除不彻底,跨用户隔离全靠一个过滤条件。权限过滤要在检索阶段完成,先按 user_id 过滤再算相似度;不能检索完再指望模型自己忽略别人的数据。

可能的追问:“向量库里的记忆怎么更新?”用稳定的 id 和主题键做 upsert,旧版本标记失效但保留历史。比如员工改说“以后走 CC-3050”,就更新 default_cost_center 这条,旧值 CC-2031 标为失效;不要再追加一条新的,否则新旧两条会一起被召回。

记忆的写入、读取、遗忘、冲突和过期怎么处理?(26/18)

简答:把记忆当成一条有生命周期的数据管线。写入要筛选和验证,读取按作用域和相关度检索,冲突按来源和时间裁决,遗忘靠 TTL、衰减和用户删除。每一步都由明确的规则驱动,不让模型随意决定。

细说:

写入的时机有三类:用户明确要求、任务结束时抽取、关键决策点。写之前问三个问题:下次还会用吗?敏感吗?经过确认了吗?模型的推测和建议不能当事实写进去。比如模型在对话里猜“员工可能经常加班打车”,这句不能写成记忆;员工自己说“以后默认走 CC-2031”,可以写,并标 confirmed: true。

读取时先按用户、租户、作用域过滤,再按相关度、时效和重要性排序,取前 k 条,注入时标注来源和时间。一种常见的打分思路来自 Generative Agents:给每条候选记忆打一个分,越新、越重要、和当前问题越相关,分越高。

score(m)=α⋅recency(m)+β⋅importance(m)+γ⋅relevance(m,q)\mathrm{score}(m)=\alpha\cdot\mathrm{recency}(m)+\beta\cdot\mathrm{importance}(m)+\gamma\cdot\mathrm{relevance}(m,q)

逐个符号看:

符号 含义 取值范围
mm 一条候选记忆
qq 当前的查询,比如员工这句“打车超标还能报吗”
recency(m)\mathrm{recency}(m) 新近度:多久没被用过了。Generative Agents 里用指数衰减 λΔh\lambda^{\Delta h},λ=0.995\lambda=0.995,Δh\Delta h 是距上次被访问过去的(沙盒游戏里的)小时数 0 到 1
importance(m)\mathrm{importance}(m) 重要性:写入时让模型打 1 到 10 分 归一化后 0 到 1
relevance(m,q)\mathrm{relevance}(m,q) 相关度:记忆向量和查询向量的余弦相似度 归一化后 0 到 1
α,β,γ\alpha,\beta,\gamma 三项的权重 原文三项都先 min-max 归一化到 0 到 1,权重都取 1

先算一下新近度衰减有多快:0.995100=e100ln⁡0.995≈e−0.501≈0.610.995^{100}=e^{100\ln 0.995}\approx e^{-0.501}\approx 0.61,也就是 100 小时(约 4 天)没用过,新近度剩六成;0.995720≈e−3.61≈0.0270.995^{720}\approx e^{-3.61}\approx 0.027,一个月没用过,几乎归零。这里 exe^{x} 是指数函数,ln⁡\ln 是自然对数,用来把幂次换成好算的形式。

再手算一次排序(数值示意,已归一化)。员工问“打车超标还能报吗”,候选有三条:

记忆 recency importance relevance α=β=γ=1\alpha=\beta=\gamma=1 γ=2\gamma=2
m1 常驻城市:上海 0.2 0.6 0.5 0.2+0.6+0.5 = 1.3 0.2+0.6+1.0 = 1.8
m2 9 月报销因缺超标事由被退回 0.5 0.8 0.9 0.5+0.8+0.9 = 2.2 0.5+0.8+1.8 = 3.1
m3 喜欢简短回复 0.9 0.3 0.2 0.9+0.3+0.2 = 1.4 0.9+0.3+0.4 = 1.6

取前 2 条。权重全为 1 时选中 m2 和 m3:m3 只是最近刚用过,就挤掉了和问题更相关的 m1。把相关度的权重 γ\gamma 调到 2,m1 得 1.8,超过 m3 的 1.6,结果变成 m2 和 m1。权重怎么定,要拿自己的评测集去调。

这个式子的设计思路是三个信号互相补位:只看相关度,会把三年前的旧偏好召回来;只看新近度,会被刚聊过的闲话占满。它也有边界。三项必须先归一化到同一个范围,否则 1 到 10 分的重要性会把 0 到 1 的余弦相似度完全压住。加权求和允许“一项极高弥补另一项极低”,一条毫不相关、但刚被访问过的记忆也可能挤进来,所以要先做作用域和权限过滤,再给相关度设一个下限。Generative Agents 的场景是游戏沙盒、按小时衰减;业务系统里“常驻城市”这种偏好根本不该随时间衰减,衰减速度要按记忆类型分别设。

冲突:同一主题出现新旧两个值时,优先级通常是“用户明确更正 > 权威业务系统 > 近期观测 > 模型推断”,旧值保留为历史版本。例如员工口述金额 120,发票系统里是 118.5,以发票系统为准;员工随后说“对,是 118.5”,更正和系统一致,冲突消解。

过期与遗忘:TTL(如“这周在北京出差”设 7 天过期)、重要性衰减、容量淘汰、用户删除请求。删除要真正删掉,并同步删索引。

容易踩的坑:写入过度会造成记忆污染,无关或错误的内容被反复召回;写入过少,每次都要重新问。记忆还可能成为间接注入的载体:一段恶意文本被写进记忆,下一次被当成可信的上下文读出来。

可能的追问:“用户说‘忘掉我刚才说的’怎么办?”删除对应的记录和索引,同时从当前会话的短期状态里移除。如果这条信息已经被用在某个已提交的动作里,要告诉用户那个动作不会因此自动撤回。

放到报销案例里:员工的常驻城市、默认成本中心适合作为长期偏好;“上周打车超标 38.5 元”是任务事实,任务结束后不应作为长期记忆反复召回。

上下文快满了,记忆怎么压缩?滑动窗口、摘要、外部存储、检索怎么组合?(88)

简答:压缩的原则是:能重新算出来的丢掉,精确的值结构化,语义性的内容做摘要,需要时再取回来。对应到方法上,滑动窗口保最近,滚动摘要保脉络,结构化状态保精确值,外部存储加检索保证能回溯。

细说:

方法 做法 保留 丢失
滑动窗口 只保留最近 N 轮 近期细节 早期约束与事实
滚动摘要 旧内容逐步合并成摘要 脉络 精确数值、原话
结构化抽取 抽成状态字段 精确值、决策 语气与细节
外部存储 + 检索 原文落库,按需召回 全部可回溯 召回不准时会漏
工具结果裁剪 大结果只留关键字段和句柄 关键字段 次要字段(可再查)

组合方案(示意):

context = system + 工具定义
        + 任务状态摘要(目标 / 计划 / 已确认事实 / 待办)
        + 滚动摘要(更早的对话脉络)
        + 检索到的记忆(按当前子任务,k 条)
        + 最近几轮原文

一个压缩前后的对比(示意)。压缩前,上下文里有第 1 到 8 轮原文,加上 query_invoices 返回的 20 个字段的 JSON。压缩后:

[任务状态] INV-102,118.5 元,标准 80 元,超标 38.5 元,依据第 4.2 条;无重复报销;事由已补。
[摘要] 员工第 1 轮口述金额 120,第 3 轮更正为 118.5;员工要求不要发私人邮箱。
[句柄] 发票原始返回:blob://t-123/step2.json
[最近 2 轮原文] ……

金额、单号进了状态,“别发私人邮箱”这个约束进了摘要,20 个字段的原始 JSON 只留句柄,要用时再取。

触发时机按 token 阈值来,比如用到窗口的七八成再压,不要每轮都压。尽量在任务的自然边界压缩,比如某个子任务刚完成时;不要切在工具调用和它的结果之间。

容易踩的坑:摘要会漂移。摘要的摘要一层层偏离原意,甚至把模型的推测变成了“事实”,比如“员工可能用个人账户打车”被摘成“员工用个人账户打车”。对策是摘要只基于原文生成、关键事实走结构化通道、原文可以回查。另一个坑是压掉了 tool_call 却留下了对应的 tool_result,消息配不上对,会导致 API 报错或者模型困惑。

可能的追问:“压缩丢了关键信息怎么发现?”离线用长对话回放,检查后期的问题对早期事实回答得对不对,比如第 20 轮问“我最开始说的金额是多少”;线上监控“用户重复提供同一信息”的频率。

记忆、RAG、上下文是什么关系?

简答:上下文是这一轮模型能看到的全部输入。RAG 往里放关于世界的知识(制度、文档),记忆往里放关于这个用户、这个任务的信息。两者都可能用检索实现,但来源、写入者、生命周期和权限都不同。

细说:

维度 上下文 RAG 知识 记忆
是什么 一次调用的输入 外部知识库 用户 / 任务相关的持久信息
谁写入 运行时每轮组装 知识管理流程(离线索引) Agent 运行中写入,用户可修改
生命周期 单次调用 跟随文档版本 会话级或长期,可过期
作用域 当前请求 全员或按部门 单用户 / 单租户
典型错误 塞太多、顺序不当 召回不准、版本过时 串用户、污染、过期仍被召回

用报销案对一下:第 4.2 条“市内交通标准 80 元”是 RAG 知识,所有 P6 员工查到的都一样,随制度版本更新;“这位员工默认走 CC-2031”是记忆,只属于他一个人;本轮上下文则是把这两样和 INV-102 的查询结果拼在一起的那一份输入,用完就扔。

容易踩的坑:不要把记忆和知识混进同一个向量索引。两者的权限和更新策略不同,混在一起,用户 A 的记忆就可能被用户 B 召回。也别说“有长上下文就不需要记忆”:长上下文不持久、不跨会话,成本还随长度上涨。

可能的追问:“Agent 自己总结的经验算记忆还是知识?”先作为待审核的记忆候选;验证过、去掉个人信息以后,才能提升为全员知识。这和 知识花园 里先候选、再审核、后发布是同一套做法。

放到报销案例里:报销制度条款来自 RAG(第 3 站),员工的成本中心来自记忆,本轮上下文把两者和发票结果拼在一起,供本站决策。

多 Agent

多 Agent 有哪些协作结构?(90)

简答:按控制权分有四种:中心化的 Supervisor(编排者分派、汇总),层级化的 Hierarchical(多层 Supervisor),去中心的 P2P / 群聊,以及一次性的路由转交(Handoff)。生产上 Supervisor 加子 Agent 最常见,因为控制和调试都最容易。

细说:

结构 控制流 优点 缺点 适合
路由 / Handoff 入口选一个 Agent 接管 简单、上下文隔离 跨领域任务要二次转交 客服分流
Supervisor 主 Agent 分派子任务、收集结果 控制集中、易追踪 主 Agent 是瓶颈,上下文累积 研究、复杂查询
Hierarchical 多层 Supervisor 支持大规模分工 层级越多信息损失越大 大型流程
P2P / 群聊 Agent 之间直接对话 灵活,适合辩论 难收敛、易绕圈、难归责 讨论、方案评审
固定流水线 按顺序传递 可预测 实际上就是 workflow 先生成再审查

拿“审计全公司上季度差旅”这个任务看 Supervisor 怎么分工(示意):主 Agent 按部门拆成销售、研发、行政三个子任务,派给三个子 Agent 并行跑;每个子 Agent 只拿到自己部门的报销单和第 4.2 条等相关条款,返回“疑似超标清单 + 证据”;主 Agent 汇总,按金额排序后交给人。如果研发部报销单太多,研发子 Agent 再按月份拆给三个孙 Agent,这就成了 Hierarchical。

容易踩的坑:别把固定流水线式的“多 Agent”说成自主协作,顺序固定就是 workflow。P2P 在生产里很少直接用,因为终止条件和责任归属都难定义。

可能的追问:“Agent 之间怎么通信?”有三种方式。共享状态(黑板、图状态);消息传递(结构化的任务和结果);工具化调用,即把子 Agent 包装成一个工具,输入任务描述,输出结构化结果。跨组织、跨厂商时可以考虑 A2A 一类协议(规范还在演进,以当前版本为准),组织内部更常用工具化调用。

什么时候用多 Agent?为什么不是“单 Agent + 多 Skill”?(87)

简答:默认用单 Agent 加多个 Skill 或工具。只有当任务确实需要并行(子任务互相独立)、上下文隔离(各自资料量大、互相干扰)、权限隔离(不同子任务需要不同权限或模型)或者独立审查时,多 Agent 的收益才可能超过它在通信和成本上的代价。

细说:可以拿四个问题来判断。

  1. 子任务之间是否基本独立?依赖强的任务拆成多 Agent,会频繁同步,交接时还会丢信息。
  2. 单 Agent 的上下文是不是真的装不下,或者不同子任务的资料会互相污染?
  3. 是否需要不同的权限或模型?比如审查者只读、执行者可写。
  4. 有没有在同等预算下做过对比评测?多 Agent 的提升要和“单 Agent 花同样多的 token”比才公平。

Skill 方案的好处是:所有信息都在同一个上下文里,没有交接损失;流程知识按需加载,成本低;调试时只有一条轨迹。多 Agent 的好处是:子 Agent 用干净的上下文专注一件事,只把压缩后的结论交回来;并行能缩短墙钟时间。

算一笔示意账:审计三个部门,每个部门的报销单和条款加起来约 6 万 token。单 Agent 串行处理,上下文要装下 18 万 token,三个部门的单据还会互相干扰;三个子 Agent 各看 6 万 token、并行跑,墙钟时间约为原来的三分之一,主 Agent 只收三份各一两千 token 的结论。总 token 数并没有省,反而多了分派和汇总的开销。

业界两种声音都有。Anthropic 介绍它的多 Agent 研究系统时,强调并行子 Agent 在广度型调研任务上的收益,同时承认 token 消耗明显更高,文中给的量级约为普通对话的 15 倍(来源)。Cognition 则主张在强耦合的任务里避免拆分,因为子 Agent 拿不到完整上下文,容易做出互相冲突的决定(来源)。两者并不矛盾:前者的任务可以并行,后者的任务强耦合。

容易踩的坑:一种错误答法是“多 Agent 更专业,所以更好”。“专业化”多数可以用 Skill(专门的指令加一个工具子集)在单 Agent 内实现,比如“财务 Skill”只带四个报销工具和一段报销流程说明。

可能的追问:“子 Agent 会继承父 Agent 的全部上下文吗?”一般不应该。只传任务描述、必要的约束和所需的资料,并要求结构化输出。全量继承既贵,又会把无关信息和潜在的注入内容一起带过去。

放到报销案例里:单个员工的报销是强依赖的短任务,单 Agent 加财务 Skill 就够了;如果任务变成“审计全公司上季度差旅”,按部门并行的子 Agent 才有意义。

角色怎么拆?planner、executor、critic、reviewer 各负责什么?(38)

简答:拆角色是为了把“决定做什么”“去做”“检查做得对不对”分开,让检查的人不被执行者的上下文和思路带偏。每个角色都要有明确的输入、输出和权限,不然只是同一个模型换了个名字在自言自语。

细说:

角色 输入 输出 权限
Planner 目标、约束、可用能力 结构化计划(步骤、依赖、判据) 无工具或只读
Executor 单个步骤 + 必要资料 结果 + 证据 该步骤所需的最小工具集
Critic 结果 + 判据 通过 / 不通过 + 具体问题 只读
Reviewer 最终产物 + 原始需求 是否可交付 只读,或交给人

放到报销场景(示意):Planner 产出前文 S1 到 S6 那份计划;Executor 只拿到 query_invoices 和 query_tickets 两个工具,去查发票和重复报销;Critic 拿着规则检查 Executor 的结论,发现“超标 38.5 元但事由为空”,返回不通过和具体原因;Reviewer 最后对照员工的原始问题,确认回答里既有“能不能报”,也有工单草稿。

拆分要有效,前提是 critic 或 reviewer 能拿到执行者没有的信号:测试结果、业务规则、原始需求、独立检索;或者换用不同的模型和提示,降低同源偏差。上面的 Critic 有用,是因为它手里有一套 Executor 没有的规则。

容易踩的坑:同一个模型、同一份上下文既执行又审查,往往倾向于认可自己。Critic 太严会无限返工,要有轮数上限和“够好即可”的判据。角色越多,交接损失也越多。

可能的追问:“critic 和人工审核是什么关系?”critic 先过滤明显的错误,减轻人的负担;高风险的结论最终仍由人确认。critic 的检查结果可以直接显示在人工审核界面上,作为一项项检查条目。

多 Agent 的通信、共享状态、上下文污染和失败模式怎么处理?(58/36/29)

简答:多 Agent 的大部分故障,根子是分布式系统问题,而不是模型能力问题:谁拥有哪份状态、消息格式明不明确、冲突怎么仲裁、什么时候停。解法也照搬分布式系统:单一事实来源、结构化消息、明确的 owner、超时和仲裁。

细说:

通信用结构化消息,至少包含 task_id、发送者、类型(请求 / 结果 / 错误)、内容和证据引用,别让 Agent 之间用自由文本“聊天”。研发部子 Agent 交回的结果大致是这样(示意):

{"task_id": "audit-q3", "subtask_id": "dept-rd", "from": "rd_auditor",
 "type": "result",
 "content": {"checked": 412, "suspicious": [
   {"ticket": "R-50317", "issue": "打车 126 元,超出 80 元标准 46 元,未填事由"}]},
 "evidence": ["INV-077", "《差旅费管理办法》2026 版第 4.2 条"]}

共享状态由编排者持有一份,子 Agent 只能更新自己负责的字段,比如 dept-rd 子 Agent 只能写 results.rd。多个子 Agent 要写同一个字段时,需要合并规则(reducer)或者锁。

上下文污染是指子 Agent 冗长的中间过程、错误的假设、被注入的文本流回了主 Agent。对策是子 Agent 只交回摘要和证据,工具结果标注为数据,主 Agent 对子结果做校验,比如上面那条,主 Agent 可以自己去查一下 R-50317 的事由字段是否真的为空。

失败 表现 兜底
重复劳动 两个 Agent 做同一件事 任务认领、去重
信息丢失 交接时漏掉约束 交接 schema、必填字段
结论冲突 子 Agent 结论相反 仲裁者按证据裁决,或交给人
递归对话 A 问 B,B 又问 A 最大轮次、调用深度、对话指纹去重
无人负责 出错后互相推 每个子任务唯一 owner

容易踩的坑:多数投票只在答案相互独立、可以比较时才有用;同源模型的“多数”,可能只是同一个错误重复了几次。三个用同一模型、同一提示的子 Agent 都说“第 4.2 条允许超标”,并不比一个说的更可信。

可能的追问:“多 Agent 怎么调试?”每次 Agent 调用是一个 span,都挂在同一条 trace 下面,记录输入摘要、输出、工具调用和耗时,见 可观测性。

框架与工程

LangChain 和 LangGraph 是什么关系?LangGraph 的图、状态、Checkpoint 怎么用?(52/41)

简答:LangChain 提供模型、工具、检索等组件的抽象,以及高层的 Agent 接口;LangGraph 是更底层的有状态编排运行时,用“状态 + 节点 + 边”描述控制流,配合 checkpointer 实现持久化、断点续跑和人工中断。2025 年发布的 1.x 版本里,LangChain 的高层 Agent 接口就构建在 LangGraph 运行时之上(API 细节随版本变化,以官方文档为准)。

细说:LangGraph 有五个核心概念。

  • State:类型化的状态对象,字段可以配 reducer。比如消息列表用“追加”而不是“覆盖”:节点返回一条新消息,它会接到列表末尾,而不会把整个列表替换掉。
  • Node:读状态、返回状态增量的函数,调模型和执行工具都是节点。比如 judge 节点读到 amount = 118.5、standard = 80,返回 {"verdict": "need_reason", "over": 38.5}。
  • Edge / 条件边:固定边,或者根据状态选择去向的条件边。模型的决策和写死的 workflow 分支就在条件边上结合起来。
  • Checkpointer:每一步之后把状态快照存下来(内存、SQLite、Postgres 等),用 thread_id 区分会话,可以从任意一个快照恢复或回溯。
  • Interrupt:在节点内暂停、等待人工输入,恢复时把人的决定传回图里。

报销案画成图(示意):

START → retrieve_policy → query_invoices_and_tickets → judge
judge --可报--> confirm(interrupt 等员工确认)--同意--> create_ticket → END
judge --信息不足--> ask_user → END(等下一轮)
judge --不可报--> explain → END

走一遍中断和恢复:图跑到 confirm 节点触发 interrupt,此时 checkpointer 已经存下了 thread_id 为 t-123 的状态快照,进程可以释放。两小时后员工点了“同意”,服务用同一个 thread_id 加上员工的决定恢复执行,图从 confirm 继续走到 create_ticket,前面的检索和查询不会重跑。

容易踩的坑:图画得太细,就成了写死的 workflow,失去灵活性;画得太粗,又回到了黑盒循环。常见做法是外层图管关键分支和确认点,节点内部用 ReAct。还有一点:恢复时被中断的那个节点会从头重新执行,所以节点里的外部写操作必须幂等。如果 confirm 节点在 interrupt 之前发过一条通知,恢复时会再发一次。框架迭代很快,面试讲概念,不要背函数名。

可能的追问:“LangGraph 的图结构和 Agent 自主决定步骤有什么区别?”图规定了允许走哪些路,条件边上的选择可以交给模型;完全自主的 Agent,就是一张只有模型和工具之间一个环的图。区别在于你把多少控制权写进了边。

手搓 Agent 还是用框架?(23)

简答:看你要的是快速覆盖常见模式,还是对循环、状态和副作用的完全控制。常见路线是原型阶段用框架验证,核心流程稳定后按需自研部分运行时。不管哪种,评测、权限和幂等都得自己负责。

细说:

维度 用框架 手搓
上手速度 快,组件齐全 慢
可控性 受抽象限制,深度定制要读源码 完全可控
调试 抽象层多、调用栈长,但有配套观测工具 代码少,问题直接
生态 现成集成多 自己接
升级风险 API 变动、依赖膨胀 自己维护
适合 原型、标准模式、团队经验少 核心业务、强合规、性能敏感

最小可用的自研运行时并不大,五块就够:消息管理、工具注册与 schema 校验、循环与终止、状态持久化、trace。前文第 2 节那段十来行的 Loop 伪代码就是它的主体。难的是边界情况:超时后的未知态、并发、恢复、流式输出和工具调用混在一起。

容易踩的坑:两种极端都会失分,一种是“框架不好用所以全自研”,另一种是“用了框架就有了 Agent 能力”。能加分的是说出你在框架里碰到的具体问题,以及怎么解决的。比如“状态里放了一个数据库连接对象,checkpointer 序列化失败”,或者“框架默认重试 3 次,和我们网关的重试叠在一起,建单接口被打了 9 次”。

可能的追问:“如果要从框架迁出,先自研哪部分?”通常先自研工具执行层和状态持久化。这两块直接关系到业务副作用、权限和审计,最需要自己能控制。

Spring AI、LangChain4j、Eino 这类框架怎么看?为什么说框架不是能力本身?

简答:它们是不同语言生态里的同类抽象。Spring AI 和 LangChain4j 面向 Java,Eino 面向 Go(来自字节跳动的 CloudWeGo 开源社区),都提供模型调用、工具调用、检索、编排等组件。框架能让接入快一些,做得对不对还要看自己的设计。

细说:选型主要看四点。

  • 团队技术栈和存量系统:Java 微服务体系里单独引入一个 Python 服务,会有额外的运维成本。
  • 对模型厂商、向量库、MCP 的支持程度(各框架都在快速跟进,以当前版本的文档为准)。
  • 编排能力:是否支持图、状态、检查点、人工中断。
  • 可观测和测试的支持。

说“框架不是能力本身”,是因为面试官想听的是你对循环、状态、工具契约、终止、失败处理和评测的理解,这些在任何框架里都要你自己做设计决策。框架帮你少写胶水代码,但不会替你决定“超标多少需要人工审批”“建单超时要不要重试”。换一个框架,这两个问题的答案不会变。

容易踩的坑:别在面试里背 API、罗列框架名。讲清楚你在某个框架里怎么落地 Loop、怎么注册工具、状态存在哪、怎么做人工确认,比报出十个框架名有用。

可能的追问:“Java 后端怎么写一个 Agent?”步骤是:定义工具(带描述和参数 schema 的方法),注册到模型客户端,由框架或自己处理工具调用循环,会话记忆按会话 id 存到外部存储,再用拦截器或切面做日志、鉴权和限流。拿 query_invoices 举例,就是写一个带注解描述的 Java 方法,参数是日期范围和类型,employee_id 从请求上下文里取,不作为模型可填的参数。思路和语言无关。

可靠性与 HITL

工具失败、超时、重试、降级和兜底,整体怎么设计?(96)

简答:分层设计。调用级管超时、重试、熔断;步骤级把错误回喂,让模型改参数或换工具;任务级走降级路径、交部分结果;系统级转人工、发告警。每一层都有明确的预算,失败信息逐层往上传,不能被吞掉。(单次失败按类型怎么处理,见上文常见追问。)

细说:

层级 机制 要点
调用级 超时、退避重试、熔断、限流 只对幂等或带幂等键的操作重试
步骤级 结构化错误回喂(错误码 + 可读原因 + 修正提示) 让模型知道“为什么失败”,而不是只看到 500
任务级 备用工具、缓存数据、只读降级、部分完成 降级结果必须标注“非实时 / 不完整”
系统级 转人工、工单、告警 附上已完成步骤和失败上下文

步骤级的回喂长什么样?对比一下(示意)。改之前,模型只看到 {"error": "500 Internal Server Error"},它不知道该重试、改参数还是放弃。改之后:

{"error_code": "INVALID_DATE_RANGE",
 "message": "date_range 跨度不能超过 31 天,当前为 92 天",
 "hint": "按月拆成多次查询,或缩小到具体出差日期"}

模型看到这条,会把查询拆成三次,而不是原样再试一遍。

调用级的重试间隔通常用指数退避加随机抖动。规则是:基础等待时间每次重试翻倍,翻到上限就不再涨,最后再乘一个随机的放大系数,让大量客户端别在同一时刻一起重试。写成式子(示意):

delayk=min⁡ ⁣(dmax⁡, d0⋅2k)⋅(1+ϵk),ϵk∼U(0, 0.5)\mathrm{delay}_k=\min\!\left(d_{\max},\ d_0\cdot 2^{k}\right)\cdot\left(1+\epsilon_k\right),\qquad \epsilon_k\sim U(0,\,0.5)

逐个符号看:

  • kk:第几次重试,从 0 开始数。
  • d0d_0:基础等待时间(秒)。d0⋅2kd_0\cdot 2^{k} 就是每多重试一次,等待翻一倍。
  • dmax⁡d_{\max}:等待时间的上限。min⁡(a,b)\min(a,b) 取两者中较小的那个,所以翻倍翻到上限就停住。
  • ϵk∼U(0, 0.5)\epsilon_k\sim U(0,\,0.5):每次重试时从 0 到 0.5 之间均匀地随机抽一个数(UU 表示均匀分布,∼\sim 读作“服从”)。乘上 1+ϵk1+\epsilon_k,等待时间就被随机放大到原来的 1 到 1.5 倍。

手算一遍:设 d0=0.5d_0=0.5 秒,dmax⁡=8d_{\max}=8 秒。

k d0⋅2kd_0\cdot 2^{k} 取 min 后 乘上抖动后的范围
0 0.5 0.5 0.5 到 0.75 秒
1 1 1 1 到 1.5 秒
2 2 2 2 到 3 秒
3 4 4 4 到 6 秒
4 8 8 8 到 12 秒
5 16 8 8 到 12 秒

比如 k=3k=3 时抽到 ϵ3=0.2\epsilon_3=0.2,就等 4×1.2=4.84\times 1.2=4.8 秒。

为什么要加抖动:发票服务挂了 10 秒,1000 个请求同时失败。如果大家都严格等 0.5、1、2 秒,就会在同一时刻一起砸回去,服务刚恢复又被打挂;加了抖动,这些重试会分散开。这个式子也有两个边界。第一,抖动乘在 min 外面,最终等待可能达到 dmax⁡d_{\max} 的 1.5 倍(上表最多 12 秒),要严格封顶,就把 min 放到最外层,或者直接在 0 到 min⁡(dmax⁡,d0⋅2k)\min(d_{\max},d_0\cdot 2^{k}) 之间均匀取值(常说的 full jitter)。第二,服务端返回了 Retry-After 时,以它为准;这个式子也只决定“等多久”,不决定“该不该重试”,非幂等的写操作照样不能重试,总次数也要有上限。

熔断:某个工具在一个时间窗口内的错误率超过阈值,就暂时断开,后续请求直接走降级,避免雪崩,也不浪费重试预算。例如 query_invoices 最近 1 分钟错误率超过 50%,接下来 30 秒内的调用直接返回“发票系统暂不可用”(数值示意)。

容易踩的坑:模型层和调用层都在重试,次数会相乘,变成重试风暴,要统一管理重试预算。降级了却不告诉用户,会把旧数据当成实时结论。把错误原文(堆栈、内部地址)直接回喂模型,既浪费上下文,又可能泄露信息。

可能的追问:“什么时候该放弃而不是降级?”降级结果会导致错误的写操作时,就放弃。比如发票系统不可用,不能凭“估计金额”建单,只能告诉用户,保留任务稍后继续。

放到报销案例里:发票查询超时,先按上表退避重试两次;仍失败,就告诉员工“发票系统暂时不可用,任务已保存,可以稍后继续”,不能凭员工口述的金额直接建单。执行层的细节见 可靠执行。

Agent 陷入死循环、循环调用或路径震荡怎么办?(51)

简答:先认清是哪种循环:同一个动作重复、A 和 B 来回震荡、还是一直探索却毫无进展。然后用“检测、打断、收口”三步处理:硬上限兜底,重复检测提前发现,打断时注入反馈或换策略,仍然没进展就停下来汇报。

细说:

现象 常见原因 检测 处理
同一调用重复 结果没进上下文、模型没读懂结果 对(工具名,规范化参数)计数 提示“已调用过,结果是……”或禁止重复
A、B 之间来回震荡 目标冲突、工具描述重叠 动作序列出现周期 指出冲突,交给规划器或用户裁决
无进展 信息确实不存在,模型不肯放弃 状态连续 N 步未变化 强制收口:报告已尝试与未找到
递归委派 多 Agent 互相调用 调用深度、往返轮数 深度上限,回到编排者
同错重试 同一错误反复出现 同类错误计数 改参、换工具或停止
sig = hash(tool_name, normalize(args))
seen[sig] += 1
if seen[sig] >= 3 or no_state_change(last_steps=4):
    inject("你已重复尝试。已知:…… 请换方法,或直接给出当前结论")
    if still_looping():
        stop_with_report()

一段真实会出现的循环轨迹(示意):

step 3: query_invoices(date_range="上周", type="taxi")   → []
step 4: query_invoices(date_range="上周 ", type="taxi")  → []
step 5: query_invoices(date_range="上周", type="Taxi")   → []

三次参数只差一个空格和大小写,normalize 去掉空格、统一小写之后签名相同,seen[sig] 到 3,运行时注入:“你已用相同条件查过 3 次,结果为空。可以换发票类型、追问员工,或者直接说明没查到。”这时模型通常会改为追问“是不是用个人账户打的车”。

容易踩的坑:重复不一定是错。轮询任务状态本来就要重复调用,这类工具要豁免,并且加上调用间隔。根因修复比打断更重要:很多循环来自工具结果被截断、错误信息不清楚、工具描述有歧义。上面那个例子,根因可能是 query_invoices 的描述里没写 type 有哪些合法取值。

可能的追问:“线上怎么发现循环?”监控每个任务的步数分布、重复调用率、触达上限的比例;对触达上限的轨迹抽样复盘,整理成回归用例,见 评测实验。

长任务怎么保证幂等?超时后不知道写操作成功没有怎么办?(29)

简答:给每个外部写操作生成稳定的幂等键,并在服务端去重。超时要当作“结果未知”,不能当作“失败”:先查询确认结果,再决定是否重试。这是长任务能安全恢复和重放的前提。(checkpoint 与状态存储见上文常见追问。)

细说:

  • 幂等键:由“任务 id + 步骤 id + 业务唯一要素”确定性地生成,重试和恢复时复用同一个键,不要每次随机生成。
  • 写操作有三种结果:成功、失败、未知。超时和连接中断都属于未知。
  • 未知态这样处理:
r = create_reimbursement_ticket(payload, idem_key=K, timeout=5s)
if r.timed_out:
    t = query_ticket_by_idem_key(K)     # 先对账
    if t.exists: mark_done(t.ticket_id)
    else:        retry(payload, idem_key=K)
  • 服务端不支持幂等键时:用先建草稿或预留、再提交的两段式,或者写之前按业务要素查重。
  • 不可补偿的操作(转账、对外发送)放在流程最后并加确认;可补偿的操作要定义好补偿动作(如撤销草稿)。

把报销案代进去:K = t-123:S6:INV-102。第一次调用 5 秒没返回;用 K 去查,查到工单 R-55821 已经存在,状态是“待审批”,直接标成 done。如果查不到,就带着同一个 K 重试,服务端看到同一个键,最多只会建一张单。

容易踩的坑:最常见的错误是“超时就重试”,结果建了两张单。另一个坑是幂等键里带了时间戳:第一次是 t-123:S6:1793613600,重试时变成 t-123:S6:1793613606,键变了,等于没有幂等。

可能的追问:“恢复后模型重新决策,会不会走出和上次不同的路?”会。所以已完成的步骤要从 checkpoint 读结果,不让模型重新决定;只有没完成的部分才继续决策。

放到报销案例里:建报销单用 task_id、步骤号和发票号集合生成幂等键,员工刷新页面或者服务重启,都不会重复提单,详见 可靠执行。

Human-in-the-loop:什么时候该询问人?怎么恢复?超时怎么办?(17)

简答:在不可逆、对外、涉及钱或权限、证据冲突、目标不清的节点暂停,向人展示要做什么、依据是什么、影响是什么,然后持久化状态、等待。人可以同意、修改或拒绝,恢复时从断点继续。超时按预先定好的策略取消或升级,绝不默认同意。

细说:

  • 触发条件由代码根据动作类型和参数判定,不由模型自评:写操作的风险等级达到阈值;金额或影响范围超过阈值;证据冲突或置信不足;所需权限超出当前授权。
  • 确认内容包括动作、关键参数(金额、对象)、依据(条款出处、发票号)、影响,以及可选项(同意 / 修改 / 拒绝)。
  • 实现上,暂停时把完整的任务状态写进 checkpoint,生成一条待确认记录;人回应后带着决定恢复,被修改过的参数要重新校验。
  • 拒绝的语义:被拒绝的动作写进状态,之后的重试、重规划都不能绕过它。
  • 超时策略:默认不执行;可以提醒、升级给其他审批人,或者让任务过期并通知用户。

一条待确认记录大致是这样(示意):

{"confirm_id": "c-901", "task_id": "t-123",
 "action": "create_reimbursement_ticket",
 "params": {"invoice_ids": ["INV-102"], "amount": 118.5,
            "reason": "客户临时改了会面地点"},
 "basis": "《差旅费管理办法》2026 版第 4.2 条;超标 38.5 元需负责人审批",
 "params_digest": "sha256:9f3c…",
 "options": ["approve", "edit", "reject"],
 "expires_at": "2026-11-03T18:00:00+08:00", "on_timeout": "cancel_and_notify"}

容易踩的坑:确认太频繁会造成确认疲劳,人开始不看就点同意。确认之后参数又被模型改了,等于没确认,所以确认过的参数要冻结,比如上面的 params_digest:执行前重新算一次摘要,和确认时的不一致就拒绝执行。等待人工不要做成同步阻塞,应该做成异步、事件驱动,否则会一直占着连接和资源。

可能的追问:“人改了参数,Agent 要不要重新规划?”如果修改影响到后续步骤,就要重新检查依赖的步骤。比如员工把金额改成了 98 元,超标变成 18 元,审批级别可能跟着变,就得重新判定。不影响的话,直接按确认后的参数执行。

放到报销案例里:建单前向员工展示:“超标 38.5 元,依据《差旅费管理办法》2026 版第 4.2 条,事由:客户临时改了会面地点,将提交给你的直属负责人审批。”员工确认后才执行。批不批是审批人的事,Agent 不代替。详见 人在回路。

怎么证明一个 Agent 可靠,而不只是 demo 跑通了?

简答:在固定的评测集上,用“任务成功率 + 过程指标 + 安全指标 + 成本”反复跑,上线后持续监控。Agent 可靠不可靠,是一个统计结论,一次演示说明不了。

细说:

  • 结果:任务成功率以外部状态为准(工单是否存在、字段是否正确)。还要看多次运行的一致性,也就是同一个任务跑 k 次全部成功的比例,它比单次成功率更能反映稳定性。
  • 过程:工具选择正确率、参数正确率、平均步数、重复调用率、触达上限的比例。
  • 安全:越权动作的尝试次数、注入样本下的不当动作率、未经确认的写操作数(应为 0)。
  • 代价:每个任务的 token、费用、P95 耗时、人工介入率。
  • 方法:黄金集(正常、边界、异常、对抗用例)+ 故障注入(工具超时、返回脏数据)+ 每次改动跑回归 + 线上抽样复盘。

“跑 k 次全部成功”为什么更严格,算一下(示意):单次成功率 90% 的 Agent,如果每次运行相互独立,同一个任务连续 3 次都成功的概率是 0.93=0.7290.9^3=0.729,只有七成出头。员工今天问一次、明天再问一次,答案不一样,他就不会再信这个助手了。

容易踩的坑:只看最终回答文本、不看外部状态,会把“嘴上说已建单、实际没建”判为成功。只测正常路径,会高估可靠性。评测集长期不更新,线上出现的新失败就看不见。

可能的追问:“用 LLM 当裁判可靠吗?”可以用来评开放文本的质量,但要有 rubric,并且和人工标注对齐校准。能用确定性检查的,比如工单 R-55821 是否存在、金额是否等于 118.5,就不要用 LLM 裁判。详见 评测/安全 和 评测实验。

放到报销案例里:一条黄金用例可以这样写:输入“上周打车超标还能报吗,能的话帮我提单”,预期是“先确认、后建单”,验收条件是工单系统里存在一张金额 118.5 元、关联 INV-102、事由非空的工单,并且确认记录早于建单时间。

连接

记忆、规划与状态、工具调用 是三个核心能力;工具/协议 决定 action space,评测/安全 决定能不能上线。

在 主线 里,本站承接 RAG 给出的带出处证据,把它变成“下一步做什么、什么时候停”的决策,再把结构化的动作提议交给 工具/协议 执行;状态、幂等和恢复的动手练习见 可靠执行,人工确认点的设计见 人在回路,多 Agent 与长任务的排障见 可观测性。

这是一篇可以继续生长的笔记。沿着关系继续阅读 ↗