Agent 核心
本文总结定义、架构、Loop、规划、记忆、多 Agent:Agent 应用岗面试问得最多的部分。
本站在主线里回答什么:员工问:“我上周出差打车超标了,还能报吗?能的话帮我提报销工单。”走到这一站时,上下文里已经有用户身份(职级 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 写得太密影响性能,太稀则恢复时要重做很多步;有外部副作用的步骤,前后都必须写。只存消息历史、不存结构化状态,恢复后模型得“重新理解”局面,很容易走出另一条路。
可能的追问:“状态在数据库里,上下文怎么恢复?”从结构化状态重新组装上下文:状态摘要加最近几轮消息,而不是回放完整历史,这也是上下文工程的一部分。幂等与“超时未知态”的细节见下文逐题作答和 可靠执行。
扩展:把一次循环写成状态更新
每一轮循环做三件事。模型看着上下文挑一个动作,工具执行这个动作并返回观测,运行时把动作和观测记进任务状态;下一轮再从新的状态重新拼上下文。写成式子是(教学抽象):
逐个符号看:
| 符号 | 读法与含义 | 报销案里是什么 |
|---|---|---|
| 第几步,从 0 开始数 | 0、1、2、3 | |
| 第 t 步开始时的持久任务状态,存在数据库里 | 前文那份任务状态 JSON | |
| 由 组装出的上下文,就是这次发给模型的消息列表。式子里省略了组装这一步,可以理解为 | system + 工具定义 + 状态摘要 + 最近几轮对话 | |
| 策略,这里就是模型本身; 是模型参数 | 你调用的那个 LLM | |
| 竖线读作“给定”:在给定上下文 的条件下,各个候选动作的概率分布。点号 是“任意一个动作”的占位 | 先查制度 0.7,先查发票 0.25,直接回答 0.05(示意) | |
| 波浪号读作“服从、从中抽取”:动作是从上面的分布里采样出来的 | search_policy(query="市内交通 超标") |
|
| 运行时执行动作,得到观测 。下标是 t+1,因为观测要到下一步才被模型看到 | 第 4.2 条的条款片段和出处 | |
| 状态更新函数,是运行时代码,不是模型 | 把条款出处写进 facts.policy_ref,把步数加一 |
手算一遍报销案的前三步(数值示意):
- : 只有目标,
facts为空。模型给出的分布里“先查制度”占 0.7,采样得到search_policy。 是第 4.2 条:P6 在上海市内交通标准 80 元,超标需写事由并经负责人审批。 写入 :standard = 80,policy_ref = 第 4.2 条。 - :模型看到条款,下一步选
query_invoices(date_range="上周", type="打车")。 返回 INV-102,金额 118.5 元。 写入 :invoice = INV-102,并且由代码算出over_amount = 118.5 − 80 = 38.5。 - :模型选
query_tickets查重, 为空列表, 记下“无重复报销”。下一步模型输出结论和草稿,等员工确认。
第 1 步里那个 0.7 也可以算一下:如果采样温度不为 0,同样的 跑 100 次,大约 70 次先查制度、25 次先查发票、5 次直接回答(示意)。所以排查线上问题要看当时记录下来的 和 ,拿同样的输入重跑一次不一定能复现。
把 和 分开写,是因为上下文会被裁剪、压缩、重新排序,状态不能丢。38.5 这个数由 用代码算出来、存在 里,后面无论上下文怎么压缩它都在。这个式子也有省略的地方:真实系统在执行 之前还要检查权限、schema 和预算;模型给出“完成”只是 的一种取值,是候选结论,要由验收器去查外部状态(工单是否真的存在)后才能让 把任务标成 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 次即停 |
| 人工中止 / 拒绝 | 用户不想继续 | 每步检查中止信号 |
把这几条合起来,就是“全部满足才继续,任何一条不满足就停”。写成教学式子:
逐个符号看:
- :已经走完的步数。:最大步数。
- :第 i 步花掉的费用(元,或换算成 token)。 读作“对所有不超过 t 的 i 求和”,就是到目前为止的累计花费。:预算。
- :任务开始到现在过去的时间(秒)。:截止时长。
- :验收器看了状态 和外部系统之后给出的“是否已完成”,值为真或假。 读作“非”, 就是“还没完成”。
- 读作“并且”,四个条件必须同时成立。
- 是指示函数:方括号里的条件成立记 1(继续),不成立记 0(停)。
手算一个例子(示意):设 , 元, 秒。前 7 步的费用依次是 0.06、0.08、0.05、0.09、0.07、0.10、0.08 元。
- 第 5 步走完: 成立;累计 成立;已用 48 秒 成立;任务还没完成, 成立。四条都成立,结果为 1,继续。
- 第 7 步走完:累计变成 , 不成立,结果为 0,停下收口。其他三条成立也没用。
这个式子有一个边界要注意:它是“走完一步再检查”,所以上面的例子实际花了 0.53 元,超了预算一步。要严格不超,就在执行下一步之前用预估费用判断,即要求 ,其中 是对下一步费用的估计。另外 必须由验收器给出,如果直接拿模型说的“完成了”来填,这个式子就没有意义了。
容易踩的坑: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 评估自己的输出或轨迹,再据此修正:重试、改计划、或者记下一条经验。它有用的前提是有外部可检验的反馈信号;只凭模型自我感觉的反思收益有限,甚至会把对的改成错的。
细说:常见有三种形态。
- 输出自修正(如 Self-Refine):先生成,再自评,再改写,适合格式、完整性、风格。例:工单草稿写完,自评发现“事由”字段空着,补上。
- 轨迹反思(如 Reflexion):任务失败后,把“哪里错了”总结成一段文字,下一次尝试时带上。例:上次因为没查重复报销被财务退回,反思写成“建单前必须先调 query_tickets”,下次带进上下文。
- 外部 critic:单元测试、schema 校验、规则引擎,或者另一个按 rubric 打分的模型,给出反馈后 Agent 修正。
可靠程度的经验排序是:确定性检查器 > 有 rubric 的独立评审 > 同一模型无依据的自评。已有研究指出,在缺少外部反馈时,大模型对推理题的自我纠正并不可靠(Huang et al., 2023)。
容易踩的坑:每轮反思都是额外的调用,成本可能翻倍;轮数要设上限,通常一到两轮。写进长期记忆的“经验教训”需要先验证,不然会把错误经验固化下来,比如一次偶然的接口超时被总结成“发票系统周一不可用”。
可能的追问:“反思和重试有什么区别?”重试是同样的动作再来一次,对付瞬时故障;反思是换个策略再来,对付决策错误。对网络超时做反思是浪费,对参数错误盲目重试也是浪费。
放到报销案例里:建单前用规则校验比让模型“再想想”可靠得多。三条规则就够:金额 118.5 不超过发票合计 118.5;事由非空;引用的第 4.2 条在制度库里真实存在。任何一条不过,直接把具体哪条没过回喂给模型。
任务怎么拆分?多意图、依赖、粒度和重规划怎么处理?(25/20)
简答:拆分的目标是得到每步可执行、可验收、依赖明确的子任务。先识别意图和它们之间的条件关系,再画依赖图;粒度按“一个子任务对应一个可验收的输出”来定;执行中一旦观测和前提不符,就局部重规划。
细说:
- 多意图:一句话里可能有多个意图,而且彼此有条件关系。“能不能报”是查询,“帮我提单”是写操作,而且以前者为条件,必须显式拆开并记下这个条件。
- 依赖:画成 DAG,没有依赖的并行,有依赖的串行。
- 粒度:太粗,一步里混了查询和写入,失败时说不清是哪里错;太细,调用次数多、计划冗长。比如“查发票并建单”是太粗,“查发票的第 1 页”“查发票的第 2 页”是太细。
- 完成判据:每个子任务写清楚什么算做完。
- 重规划:子任务失败、结果推翻了前提、用户补充了新信息,只重排受影响的子图,已完成的部分保留。
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)
简答:短期记忆是当前会话和任务的工作状态,不等于原始聊天记录。每轮要更新的是结构化的任务状态(目标、计划进度、已确认的事实、待确认的事项)和最近几轮原文,其余的用摘要或句柄代替。
细说:短期记忆通常包含四类内容。
- 对话窗口:最近 N 轮原文,保留语气和指代。员工说“就那张”,得靠上一轮原文才知道指的是 INV-102。
- 工作状态:目标、计划、步骤状态、关键的中间结论。
- 工具结果:原文放外部存储,上下文里只留关键字段、摘要或句柄。
- 约束与承诺:用户说过的限制(“别用我私人邮箱”),以及 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:给每条候选记忆打一个分,越新、越重要、和当前问题越相关,分越高。
逐个符号看:
| 符号 | 含义 | 取值范围 |
|---|---|---|
| 一条候选记忆 | ||
| 当前的查询,比如员工这句“打车超标还能报吗” | ||
| 新近度:多久没被用过了。Generative Agents 里用指数衰减 ,, 是距上次被访问过去的(沙盒游戏里的)小时数 | 0 到 1 | |
| 重要性:写入时让模型打 1 到 10 分 | 归一化后 0 到 1 | |
| 相关度:记忆向量和查询向量的余弦相似度 | 归一化后 0 到 1 | |
| 三项的权重 | 原文三项都先 min-max 归一化到 0 到 1,权重都取 1 |
先算一下新近度衰减有多快:,也就是 100 小时(约 4 天)没用过,新近度剩六成;,一个月没用过,几乎归零。这里 是指数函数, 是自然对数,用来把幂次换成好算的形式。
再手算一次排序(数值示意,已归一化)。员工问“打车超标还能报吗”,候选有三条:
| 记忆 | recency | importance | relevance | ||
|---|---|---|---|---|---|
| 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。把相关度的权重 调到 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 的收益才可能超过它在通信和成本上的代价。
细说:可以拿四个问题来判断。
- 子任务之间是否基本独立?依赖强的任务拆成多 Agent,会频繁同步,交接时还会丢信息。
- 单 Agent 的上下文是不是真的装不下,或者不同子任务的资料会互相污染?
- 是否需要不同的权限或模型?比如审查者只读、执行者可写。
- 有没有在同等预算下做过对比评测?多 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": "按月拆成多次查询,或缩小到具体出差日期"}
模型看到这条,会把查询拆成三次,而不是原样再试一遍。
调用级的重试间隔通常用指数退避加随机抖动。规则是:基础等待时间每次重试翻倍,翻到上限就不再涨,最后再乘一个随机的放大系数,让大量客户端别在同一时刻一起重试。写成式子(示意):
逐个符号看:
- :第几次重试,从 0 开始数。
- :基础等待时间(秒)。 就是每多重试一次,等待翻一倍。
- :等待时间的上限。 取两者中较小的那个,所以翻倍翻到上限就停住。
- :每次重试时从 0 到 0.5 之间均匀地随机抽一个数( 表示均匀分布, 读作“服从”)。乘上 ,等待时间就被随机放大到原来的 1 到 1.5 倍。
手算一遍:设 秒, 秒。
| 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 秒 |
比如 时抽到 ,就等 秒。
为什么要加抖动:发票服务挂了 10 秒,1000 个请求同时失败。如果大家都严格等 0.5、1、2 秒,就会在同一时刻一起砸回去,服务刚恢复又被打挂;加了抖动,这些重试会分散开。这个式子也有两个边界。第一,抖动乘在 min 外面,最终等待可能达到 的 1.5 倍(上表最多 12 秒),要严格封顶,就把 min 放到最外层,或者直接在 0 到 之间均匀取值(常说的 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 次都成功的概率是 ,只有七成出头。员工今天问一次、明天再问一次,答案不一样,他就不会再信这个助手了。
容易踩的坑:只看最终回答文本、不看外部状态,会把“嘴上说已建单、实际没建”判为成功。只测正常路径,会高估可靠性。评测集长期不更新,线上出现的新失败就看不见。
可能的追问:“用 LLM 当裁判可靠吗?”可以用来评开放文本的质量,但要有 rubric,并且和人工标注对齐校准。能用确定性检查的,比如工单 R-55821 是否存在、金额是否等于 118.5,就不要用 LLM 裁判。详见 评测/安全 和 评测实验。
放到报销案例里:一条黄金用例可以这样写:输入“上周打车超标还能报吗,能的话帮我提单”,预期是“先确认、后建单”,验收条件是工单系统里存在一张金额 118.5 元、关联 INV-102、事由非空的工单,并且确认记录早于建单时间。
连接
记忆、规划与状态、工具调用 是三个核心能力;工具/协议 决定 action space,评测/安全 决定能不能上线。
在 主线 里,本站承接 RAG 给出的带出处证据,把它变成“下一步做什么、什么时候停”的决策,再把结构化的动作提议交给 工具/协议 执行;状态、幂等和恢复的动手练习见 可靠执行,人工确认点的设计见 人在回路,多 Agent 与长任务的排障见 可观测性。