在近年来的大模型系统设计面试中,“请设计一个生产级 AI Agent 的对话与执行流程”是一道极为高频的高杠杆题目。
许多候选人一提到状态,往往只能给出前端维度的 isLoading,或是简陋的 thinking / acting / finished。但在真实复杂业务场景(例如需要多轮工具调用、人工审批流、网络中断恢复、超时重试以及处理退款转账等副作用)下,单薄的状态枚举会瞬间让系统陷入竞态或不一致。
回答这道题的核心,是理清两个根本问题:
- 当前执行到了哪一步?(生命周期流转与调度)
- 从这里继续执行需要哪些确定性数据?(上下文一致性、检查点与副作用屏障)
本文从零拆解生产级 Agent 对话运行时的三层状态模型,并系统推导状态流转、并发与中断恢复的严密闭环。
🏛️ 核心骨架:Session、Run 与 Tool Call 的三层分治
不要把所有状态塞进同一个上下文对象中。一个具备容灾能力的运行时,必须在抽象上完成三层解耦:
| 层次 | 职责与生命周期 | 核心持久化数据 |
|---|---|---|
| Session(会话层) | 用户与 Agent 持续交互的长周期容器 | 消息历史(Messages)、用户偏好、当前活跃 Run 引用、租约信息 |
| Run(单次执行层) | Agent 为推进一次具体诉求而运行的任务实例 | 顶层状态、活动阶段(Phase)、执行预算、等待上下文、错误原因 |
| Step / Tool Call(步骤与工具层) | 一次模型推理或单次具体工具调用的执行原子 | 输入参数、输出结果、尝试次数(Attempts)、幂等键(Idempotency Key) |
核心分界线:一次 Run 结束,绝不等于 Session 结束
例如用户提出:“帮我查下这个订单,如果符合条件就退款。”
- 这是一个 Run。在此次执行中,Agent 可能先查询订单,发现满足条件,挂起等待用户确认;用户点击确认后,唤醒该 Run 完成退款并返回最终结果。
- 用户随后说:“顺便再查一下另外那个订单。” 这时是在同一个 Session 下创建了一个全新的 Run。
- 对挂起中的任务补充信息或提交审批,是**恢复(Resume)**原有 Run,而不是无休止地重建执行实例。
职责边界约束大模型可以提出“请求调用退款工具”,但是否执行、何时执行、如何流转必须由运行时(Runtime)强控制。绝不能因为模型在输出文本里写了一句“已获管理员批准”,运行时就绕过审批机制直接触发扣款。
🔄 Run 的生命周期与状态流转机理
一个完备的 Run 顶层生命周期不需要将所有动作都枚举为状态。六个状态即可构成严密的有限状态机(FSM):
1. 顶层状态与内部阶段(Phase)解耦
| 状态 (Status) | 语义与退出条件 | 内部阶段 / 等待原因 (Phase / Reason) |
|---|---|---|
queued | 任务已入队,等待排队调度或工作者拉取 | waiting_worker |
running | 运行时正在主动向前推进计算或 I/O | preparing_context / calling_model / executing_tools |
waiting | 执行挂起,依赖外部事件驱动唤醒 | waiting_input / approval / retry_backoff / reconciliation |
completed | 本次 Run 正常结束(终态) | 无(清理活动上下文) |
failed | 触发不可恢复错误,或重试预算耗尽(终态) | 记录致命错误上下文 |
cancelled | 收到用户或系统取消指令,且已安全停止(终态) | 保留已生效操作,清理未执行任务 |
2. 流式输出(Streaming)是事件,不是顶层状态
前端界面呈现的打字机效果、思维链展开,本质上是模型调用的增量事件(text_delta、tool_call_delta)。在服务端,Run 依然稳定处于 running 状态(Phase 为 calling_model)。
在流式结束的那一刻,系统必须完成关键校验:模型是否输出完整?工具参数是否被截断? 半截 JSON 绝不可流入执行管道。
3. completed 衡量的是执行过程,而非业务意图的达成
查询结果显示“订单不满足退款要求”,Agent 礼貌告知用户无法退款,本次 Run 依然应当正常标记为 completed。业务逻辑拒绝不是系统运行故障,绝不能随意标为 failed。
💾 故障容灾底座:崩溃恢复需要持久化哪些数据?
假设执行器在工具刚刚发起的瞬间遭遇断电重启,数据库里如果仅有一行 status = "running",恢复进程将面对一片迷雾:工具到底调用出去没有?是否需要重新调模型?从哪一步续跑?
因此,生产环境必须实现确定性的检查点(Checkpoint)持久化:
| 数据分类 | 核心字段设计 | 容灾解决的问题 |
|---|---|---|
| 关联标识 | session_id, run_id, step_id, tool_call_id | 准确定位上下文归属,保证分布式跟踪链不断裂 |
| 持久化历史 | messages, role, content, tool_results | 记录已经被业务认可的事实,不随单次上下文压缩丢失 |
| 活动步骤快照 | current_step, pending_tools, snapshot_version | 重启后精确定位恢复点,防止已成功步骤重复执行 |
| 等待上下文 | wait_type, request_id, expire_at, resume_target | 节点重启后,收到审批或输入回调时知道唤醒哪个挂起操作 |
| 执行熔断预算 | model_call_count, token_usage, timeout_deadline | 设立刚性防线,防止死循环与 Token 账单爆炸 |
| 分布式租约 | lease_worker_id, lease_token, lease_expires_at | 防止旧节点假死唤醒后,双写竞争推进同一个 Run |
工程原则:对话持久化历史 模型请求上下文对话历史(Session History)是业务层面的不可变事实依据;而模型上下文(Model Context)是针对特定模型窗口组装的瞬时切片(包含剪裁、历史压缩、RAG 注入)。压缩上下文或做滑动窗口时,绝不能反向修改甚至删减持久化数据库中的真实消息记录。
⚡ 工具调用的并发控制与超时未知边界
当大模型在一轮推理中并发请求了两个工具:query_user_level(只读)与 execute_refund(写副作用),Run 级别的单一状态无法表达内部并发进度,必须为每个 Tool Call 引入独立生命周期:
超时 失败:极其致命的副作用边界
这是面试官最喜欢追问的高阶工程点:退款接口网络超时(HTTP 504),能直接重试吗?
绝对不能!HTTP 504 仅代表网关未收到下游回复,底层的扣款动作极有可能已经落库成功。若盲目重试,将导致灾难性的资金重复扣减。
生产级 Agent 对待写副作用的防御准则:
- 引入
unknown(状态未决)状态:超时的工具调用立即挂起,进入unknown状态。 - 强制显式业务幂等键(Idempotency Key):在首次向外部系统发出网络调用前,运行时必须先持久化业务幂等键。即使模型在下一轮重新输出了新的
tool_call_id,底层业务调用的幂等凭据始终唯一。 - 主动对账机制(Reconciliation):状态未决的调用不能让 Agent 瞎猜,系统应调度专门的查询对账任务;若下游不支持查询,则将 Run 置为挂起并报警,引入人工核查兜底。
🛑 用户插话、取消与故障恢复策略
1. 用户插话(User Interruption)
若在 Agent 执行耗时任务过程中用户突然输入新消息,若直接并发推进,会导致同一会话的上下文产生分叉。
- 排队与意图识别:单 Session 强约束同一时刻仅有一个活动 Run,其余输入入队暂存。
- 插话安全点拦截:Agent 只能在特定的安全边界(例如下一次模型调用前、下一个工具启动前)读取插话。
- 区分插话类型:
- 补充条件(“补充一下,订单号是 9527”):注入当前上下文,继续执行;
- 意图切换(“算了别查了,帮我查查天气”):触发当前 Run 取消,清空后续步骤并开启新 Run。
2. 取消(Cancellation):发起 生效
用户点击“取消”按钮时,系统不能原地将其标为 cancelled。
- 首先将请求标记为
cancel_requested_at; - 向下游执行线程广播
AbortSignal; - 等待正在执行的网络 I/O 优雅中断或退出,确保没有空中飞行的写操作后,Run 才正式落盘为终态
cancelled; - 已经产生外部副作用的步骤(如已扣款)绝不会随取消自动回滚,界面必须清晰标明已生效操作。
3. 崩溃恢复(Restart Recovery)
服务节点宕机重启后,恢复程序按如下优先级处理中断的 Run:
1. 校验租约令牌:确认原执行节点已超时失联,抢占执行权;2. 读取检查点: ├── 仅收到半截模型响应 ──> 废弃残缺片段,在预算允许范围内重新发起 LLM 调用; ├── 工具调用已完成落库 ──> 直接复用已有结果,跳过重复执行; ├── 工具调用处于 unknown ─> 触发对账管道,禁止自动重放; └── 处于 waiting 审批 ──> 校验审批时效,继续等待用户交互事件。🎯 场景端到端推演:一笔退款业务的严密闭环
我们用一笔典型的“订单退款”交互,来完整验收上述状态模型的自洽性:
| 阶段 | Run 状态 / Phase | 工具状态 (Tool Status) | 系统核心事实与一致性动作 |
|---|---|---|---|
| 1. 接收诉求 | queued | - | 接收“查询订单并申请退款”,分配全局 run_id |
| 2. 开始推演 | running / preparing_context | - | 组装模型上下文,加载可用工具清单与扣减预算 |
| 3. 查询订单 | running / executing_tools | query_order: running | 派发只读查询,生成只读步骤记录 |
| 4. 决策退款 | running / calling_model | query_order: succeeded | 模型感知订单符合条件,输出调用 refund 的请求 |
| 5. 触发拦截 | waiting / approval | refund: pending | 拦截写副作用,生成 approval_id 并挂起,等待人工授权 |
| 6. 用户授权 | running / executing_tools | refund: running | 用户点击“同意”,持久化幂等键并向支付网关发起请求 |
| 7. 遇到超时 | waiting / reconciliation | refund: unknown | 支付网关超时,挂起 Run,切入对账等待 |
| 8. 对账完成 | running / preparing_context | refund: succeeded | 对账服务确认扣款成功,回写最终数据 |
| 9. 达成闭环 | completed | - | 模型向用户输出“已为您完成退款”,清理活动租约 |
总结:面试回答的标准落地模板
在面试中拆解这套设计时,可以按照以下节奏递进输出:
- 先给结论,拉开层次:建立 Session(长周期容器)、Run(单次交互运行时)、Tool Call(原子执行单元) 的三层正交抽象。
- 给出核心状态机:用
queued / running / waiting / completed / failed / cancelled明确生命周期,并将活动阶段与挂起原因下沉到内部字段。 - 点透高阶边界与防御机制:
- 流式输出是事件,不是顶层状态;
- 工具超时是
unknown,必须配合幂等键与对账,防止副作用二次放大; - 检查点持久化与分布式租约,是实现断点续跑与防脑裂的根本基石。
把以上边界和一致性推导清晰,就能向面试官充分证明:你不仅懂得如何 Prompt 调用大模型,更具备设计高可用、高可靠企业级分布式 Agent 运行时的全局工程把控力。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时