主题
Agent 系统架构
一个最小 Agent 只有三件事:模型、工具和循环;一个可生产的 Agent 还需要状态、预算、权限、恢复、观测和评测。把这些责任全藏在一段系统提示词里,会得到一个很会解释、却难以控制的系统。
1. Agent 的最小控制循环
最小控制循环 · 每轮执行
- 1目标 + 当前状态人负责人定义目标,运行时维护状态
- 2模型选择动作AI 执行调用工具 / 请求信息 / 给出结果
- 3运行时验证并执行检查点校验权限与参数,再真正执行
- 4观察结果写入状态自动触发循环推进,直到满足终止条件
模型不是循环本身。Agent 运行时负责把模型输出解析为动作、验证权限、执行工具、记录观察和判断硬终止条件。
ts
type Action =
| { kind: "tool"; name: string; args: unknown }
| { kind: "ask_user"; question: string }
| { kind: "finish"; answer: string };
type RunState = {
goal: string;
step: number;
status: "running" | "waiting" | "succeeded" | "failed";
observations: Array<{ step: number; summary: string; artifactIds: string[] }>;
budget: { maxSteps: number; deadline: number; remainingCost: number };
};状态应可序列化和持久化。进程重启后能从上一个安全检查点继续,而不是依赖内存里的聊天数组。
2. 先判断是否真的需要 Agent
使用确定性工作流,当:步骤固定、合规要求强、输入结构稳定、每个分支可提前枚举。使用 Agent,当:中间结果会显著影响下一步、任务需要跨多个工具探索、规则难以完整编码但结果可验证。
常见的最佳结构是混合式:
text
程序:鉴权与建单
→ Agent:调查资料并提出方案
→ 程序:Schema 与规则校验
→ 人工:批准高影响动作
→ 程序:执行与审计模型负责开放判断,程序包围确定性边界。
3. 用显式状态机代替无限对话
3.1 推荐状态
| 状态 | 允许动作 | 退出条件 |
|---|---|---|
planning | 读上下文、拆目标 | 产生可验证计划 |
executing | 调用当前步骤允许的工具 | 获得结果或错误 |
verifying | 运行检查、比较验收标准 | 通过、修复或升级 |
waiting_approval | 展示影响和待确认参数 | 用户批准或取消 |
succeeded | 生成交付摘要 | 终态 |
failed | 保存证据和恢复建议 | 终态 |
不同状态暴露不同工具,比把所有工具同时交给模型更安全,也能减少选择干扰。
3.2 状态转移由谁决定
低风险语义分支可由模型建议,高风险转移由代码决定。例如模型可以建议“需要退款”,但程序必须检查订单状态、权限和金额,再进入审批状态。状态变化应记录原因与触发者。
4. 规划策略的取舍
4.1 边做边规划
每一步根据最新观察选择下一个动作。适合探索和排错,能快速适应变化;缺点是容易局部贪心或循环。
4.2 先计划再执行
先输出步骤、依赖和验收,再逐步执行。适合变更代码和多阶段研究;缺点是初始计划可能基于错误假设。执行后需要允许小范围重规划。
4.3 分层规划
上层维护里程碑,下层只规划当前里程碑的几个动作。它兼顾全局方向和局部适应性,是长任务更稳定的做法。
计划项应包含 expected_observation 和 verification,而不只是动作名称:
json
{
"step": "运行类型检查",
"tool": "run_command",
"expected_observation": "退出码为 0",
"verification": "保存命令、退出码和 stderr 摘要"
}5. 观察不是原始输出堆积
工具可能返回几万行日志。全部塞回模型会耗尽上下文,还会放大提示词注入风险。运行时应把观察分为:
- 原始产物:保存在文件或对象存储,通过 ID 引用;
- 结构化结果:退出码、命中数量、错误类别;
- 面向模型的摘要:只包含决定下一步所需部分;
- 面向审计的记录:工具、参数摘要、调用者、时间和结果。
摘要不能丢失关键错误行。可先由确定性解析器提取 exit code、文件名和诊断,再让模型压缩自然语言部分。
6. 硬终止条件必须在模型之外
至少设置:
ts
function shouldStop(s: RunState): string | null {
if (s.step >= s.budget.maxSteps) return "step_budget_exhausted";
if (Date.now() >= s.budget.deadline) return "deadline_exceeded";
if (s.budget.remainingCost <= 0) return "cost_budget_exhausted";
return null;
}还要检测重复动作:相同工具和参数连续执行、错误签名重复出现、计划在两个状态间来回切换。触发时不应假装完成,而要输出当前证据、已尝试方案和需要的人工决策。
7. 写操作与人工确认
按影响分级:
- 只读:搜索、读取、预览,可自动执行;
- 可逆写入:创建草稿、修改工作区文件,可自动但需保留 Diff 和回滚;
- 外部影响:发消息、创建工单、部署,执行前展示对象与影响;
- 高影响不可逆:付款、删除生产数据、权限变更,需要强审批或不用 Agent 执行。
确认应绑定具体动作和参数摘要,不能用一次“全部允许”覆盖之后未知的操作。工具执行前再次鉴权,避免确认与执行之间资源状态变化。
8. 可恢复执行
每一步遵循“先记意图,后执行,再记结果”:
- 写入待执行动作、幂等键和状态版本;
- 原子地领取该动作;
- 执行工具;
- 保存结果或明确的未知状态;
- 推进状态机。
若网络在写操作后断开,运行时必须先用幂等键查询结果,不能盲目重试。状态更新使用乐观锁,防止两个 Worker 同时推进同一个 Run。
9. 一个简化运行器
ts
async function runAgent(state: RunState): Promise<RunState> {
while (state.status === "running") {
const reason = shouldStop(state);
if (reason) return { ...state, status: "failed" };
const context = await buildWorkingContext(state);
const action = await chooseAction(context);
const checked = await validateAction(action, state);
if (checked.kind === "finish") {
await verifyAcceptance(checked.answer, state);
return { ...state, status: "succeeded" };
}
if (checked.kind === "ask_user") {
return { ...state, status: "waiting" };
}
const result = await executeWithIdempotency(checked, state);
state = await appendObservationAndCheckpoint(state, result);
}
return state;
}真实实现还需要错误分类、取消信号、并发控制和 Trace,但责任边界不应变化:模型提出动作,运行时验证动作,工具执行动作,验收器判断是否完成。
10. 架构审查清单
- 是否能画出全部状态和高风险转移?
- 模型声称“完成”后,是否还有独立验收?
- 进程中断后,能否判断最后一个写操作是否成功?
- 每个工具是否最小权限、参数明确、结果结构化?
- 是否存在步数、时间、成本和重复动作上限?
- 能否从一次失败追到具体模型、上下文、工具和状态版本?
这些问题都能回答后,再讨论更复杂的规划算法或多 Agent 协作。