Agent 不是会调工具的聊天机器人,是断电了还能续跑的执行系统
用户让它"把昨天的调研整理成文章,存草稿,加入专题,发布出去"。
它搜了资料,写了初稿,保存了草稿,然后在"加入专题"这一步,模型调用超时了。
你觉得它该从断点接着跑,还是从头再来一遍?
如果从头再来——它会再存一篇草稿,再扣一次费,可能再发一次文。用户第二天看到的,是草稿箱里躺着两篇一模一样的文章,专栏里挂着一条重复的发布记录。
这个场景不是虚构的。只要一个 AI 系统拥有"调用工具、改数据"的能力,它就不再只是生成文字,而是在运行一个程序。而程序,就有断电、超时、重复执行、状态不一致这些毛病。
所以生产级的 Agent 从来不是"更聪明的提示词 + 更多工具"。它是一套受约束、可追踪、可暂停、可恢复的执行系统。
一句话概括它和普通聊天的区别:
LLM 负责提出判断和计划,真正的系统负责控制它"能看到什么、能做什么、失败后怎么办"。
下面这篇文章,就是用一张总领链路图开头,一节一节把这条链路拆开讲。每一节都有一张局部的链路设计,最后再拼回一张完整的图。
先看整条链路(总领)
不要急着看代码,先记住这张图。这篇文章的所有章节,都是这张图里的某一段。
用户目标
│
▼
AIApplicationService ← 编排一次"任务",不是拼接一次 Prompt
│
▼
ContextBuilder ← Context Compiler:身份 → 检索 → 去重 → 排序 → 过滤 → Token 预算
│
▼
LLMGateway ← 模型选择 / 重试 / 超时 / 计费;只返回"建议",不碰真实系统
│ 工具调用请求
▼
ToolGateway ──► PolicyEngine ← 权限 / 风险分级 / 审批 / Schema / 限额
│
▼
UseCase → Domain → DB ← 真正执行业务;返回 Receipt
│
▼
AgentRun ← 每步落 Checkpoint,断了续跑,问"为什么"可 Replay
这张图里最关键的一件事:模型只占中间小小一格。 判断权归它,但执行权不在它手上。它所有的"想法"都以请求的形式发出去,由外面这一圈确定性的系统来裁决和落地。
链路的最外层,还有一条不画进来的回路——Promotion:线上执行的 Trace 沉淀成候选案例,经过离线评估和人工审核后,才被晋升为新的策略。这条回路是让系统"越用越准"但又不至于自己乱改行为的钥匙,后面第七章专门讲。
一、Agent Runtime:模型的"大脑",系统的"操作系统"
先建立一个直觉。
普通 AI 对话是条单向流水线:
用户问题
│
▼
拼接 Prompt
│
▼
调用模型
│
▼
返回文本
Agent 则是多跳的:
用户目标
│
▼
模型分析
│
▼
搜索知识库 → 读取文件 → 创建笔记 → 修改数据 → 返回结果
差别不在"多调了几次模型",而在于:第二步开始,系统在真实世界里留下了痕迹。 创建了笔记、改了数据,这些动作是不可撤销的、有副作用的、要花钱的。
于是需要一个 Agent Runtime,专门管下面这些事:
- 给模型哪些上下文
- 允许调用哪些工具
- 工具参数是否合法
- 是否需要用户审批
- 调用失败是否重试
- 如何记录整个执行过程
- 中断后如何继续
它和操作系统的职责一模一样:你不是给每个进程发一把内核钥匙,而是让它通过系统调用去申请资源,由内核裁决。所以:
LLM = 大脑,负责推理
Agent Runtime = 操作系统,负责约束和执行
边界要怎么立
落实到代码,第一步是划出四条边界,任何业务代码都不得越过去:
public interface AgentRuntime {
AgentRun start(AgentCommand command); // 开启一次任务,先落运行记录
AgentRun resume(String runId); // 断电/超时后,从断点续跑
AgentTrace trace(String runId); // 拿回放凭证,回答"为什么"
}
public class AIApplicationService {
private final ContextBuilder contextBuilder;
private final LLMGateway llmGateway;
private final ToolGateway toolGateway;
private final AgentRunStore runStore;
public AgentResult handle(AgentCommand cmd) {
// 1. 先建运行记录 —— 任务是"可以放回放的",不是一次性的
AgentRun run = runStore.create(cmd);
// 2. 编译上下文(第二节)
CompiledContext ctx = contextBuilder.compile(cmd.asRequest());
// 3. 让模型"建议"下一步 —— 它只表达意图
LLMResponse resp = llmGateway.complete(ctx, run);
// 4. 建议不直接执行,交给 ToolGateway 走策略门卫(第三节)
List<ToolReceipt> receipts = resp.proposedCalls().stream()
.map(toolGateway::execute)
.toList();
run.markCompleted(receipts);
return AgentResult.of(resp.answer(), receipts);
}
}
这条链路里,AIApplicationService 是唯一入口。它不允许出现的事是——某个业务方法里直接 new OpenAIClient()、直接 noteRepository.delete()、直接绕过权限系统。
这条链路的形状:
Controller
│
▼
AIApplicationService
│
▼
ContextBuilder → LLMGateway → 模型供应商
│
└──► ToolGateway → UseCase → Domain
二、Context 是编译出来的,不是临时拼接的
很多初期系统是这么拼 Prompt 的:
系统提示词
+ 用户问题
+ 最近聊天记录
+ 搜索到的笔记
+ 用户资料
= 最终 Prompt
看着能跑,跑一段时间问题全冒出来:
- 上下文越来越长,不知道哪些内容真正重要
- 私密内容可能错误地进了 Prompt
- 旧笔记和新笔记互相冲突,模型不知道该信谁
- 摘要模块查一遍笔记,问答模块又查一遍,各拼各的
- Token 成本失控,没人说得清一次调用花了多少
- 出错了,谁也不知道模型当时到底看到了什么
问题不在"拼接"这个动作,而在没有一层东西统一负责"拼"。所以就有了 Context Compiler——一个专门的上下文构建层。
它和编译器的思路是同一套:
原始资料 → 检查、转换、优化 → 可执行输入
编译管道的链路
用户请求
│
▼
Context Compiler
├─ 身份和权限检查 ← 先确认"能看到什么",再决定"要查什么"
├─ 任务类型判断 ← 这次是问答、摘要还是写作,指令模板不同
├─ 知识检索 ← 统一走 ContextService,各模块不许自己查
├─ 内容去重 ← 同一篇文档出现在多个来源,只留一份
├─ 相关度排序
├─ 新鲜度判断 ← 旧笔记降权,冲突时新版本优先
├─ 敏感信息过滤 ← 该脱敏的脱敏,该剔除的剔除
├─ Token 预算分配 ← 预算不够先砍证据,不砍权限
└─ 上下文压缩
│
▼
结构化 Context
│
▼
LLM
这里最值得记住的设计原则是:stage 的顺序即策略。 身份检查永远在最前面——你不能在不知道"这人是谁"之前就去检索;Token 预算永远在靠后的位置——先保证内容对,再压缩体积。
代码怎么写
不要给每个 AI 功能发一份自己的查笔记逻辑:
AI 摘要模块自己查笔记 ✗
AI 问答模块自己查笔记 ✗
AI 写作模块自己查笔记 ✗
Agent 自己查笔记 ✗
统一收敛到 ContextBuilder 这一个管道。
public record ContextRequest(
String userId,
String workspaceId,
TaskType taskType, // SUMMARIZE / QA / WRITING / AGENT
String currentNoteId,
String query,
int tokenBudget
) {}
public record CompiledContext(
String instructions, // 按任务类型渲染的系统指令
UserContext userContext, // 身份、偏好、可见范围
ConversationContext conversation, // 最近的对话
List<Evidence> knowledgeEvidence, // 检索证据,每一条都带来源和时间
List<String> citations, // 引用清单,供回执和审计
PermissionScope permissions, // 这个上下文允许触碰的边界
String contextVersion // 快照版本,Replay 时靠它还原现场
) {}
public interface ContextStage {
void apply(ContextRequest request, CompiledContext.Builder builder);
}
public class ContextBuilder {
private final List<ContextStage> stages;
public CompiledContext compile(ContextRequest request) {
var builder = CompiledContext.builder();
stages.forEach(stage -> stage.apply(request, builder));
return builder.build();
}
}
// 注册顺序就是编译顺序,谁先谁后是设计,不是巧合
new ContextBuilder(List.of(
new IdentityStage(), // 你是谁、能看什么 —— 永远第一位
new TaskRouterStage(), // 这次是问答还是写作
new RetrievalStage(), // 统一查知识库
new DedupStage(), // 重复内容只留一份
new RelevanceStage(), // 按相关度排序
new FreshnessStage(), // 旧内容降权
new SensitiveFilterStage(), // 该脱敏的脱敏
new TokenBudgetStage(), // 预算不够先砍证据
new CompressStage() // 最后压进窗口
));
这样做的收益是:模型怎么换、RAG 怎么改、压缩算法怎么升级,都不需要动业务代码——它们全被挡在 ContextBuilder 这一层后面。
三、Agent 可以申请执行,但不能自己决定有没有权限
先看一个错误设计:
Agent
│
▼
DeleteNoteTool
│
▼
删除笔记
然后在 Prompt 里写一句"没有用户同意,不要删除笔记"。
Prompt 只是软约束,不是安全机制。 模型可能误判用户的意图,可能被注入的提示词误导,也可能就是状态不对产生幻觉。你把安全建立在"模型每次都记得这句叮嘱"上,等于把门锁建在"每个人都自觉关门"上。
生产级的做法是加一道硬门卫:
Agent
│ 提出工具调用请求
▼
Tool Gateway
│
▼
Policy Engine
├─ 当前用户是谁
├─ 是否拥有权限
├─ 操作是否高风险
├─ 是否需要审批
├─ 参数是否符合 Schema
└─ 是否超过调用限额
│
▼
真正执行工具
核心思想就一句:
Agent 可以申请执行,但不能自己决定自己有没有权限。
就像普通用户不能因为说"我是管理员",系统就允许他删数据。判断权限的地方是 PolicyEngine,不是模型。
代码怎么写
public class ToolGateway {
private final PolicyEngine policy;
private final ToolRegistry tools;
public ToolReceipt execute(AgentId agentId, ToolInvocation call) {
PolicyDecision decision = policy.decide(agentId, call);
if (decision.blocked()) {
if (decision.requiresApproval()) {
throw new ApprovalRequired(decision.approvalId());
}
throw new AccessDenied(decision.reason());
}
// 只有策略放行了,才真正执行
return tools.get(call.toolName()).execute(call.payload());
}
}
public record PolicyDecision(
boolean allowed,
RiskLevel risk,
String approvalId, // 需要审批时,返回凭证给上游
String reason
) {}
策略判定的基础是风险分级。把工具按危害面分三档:
public enum RiskLevel {
LOW, // search_notes / read_note / list_topics / get_related_notes
MEDIUM, // create_draft / update_note / add_tag / move_note
HIGH // delete_note / publish_content / bulk_update / change_permissions / export_private_data
}
低风险直接放行,中风险要校验权限和限额,高风险必须走审批——而且要落到代码上,不是靠提示词:
public class ApprovalService {
// 申请:高风险操作先领一张审批凭证,此时还不执行
public ApprovalTicket request(AgentId agentId, ToolInvocation call) {
return new ApprovalTicket(UUID.randomUUID().toString(), agentId, call);
}
// 确认:同一张凭证只能消费一次(CAS),防重复确认
public ApprovalResult confirm(String ticketId, String userId) {
ApprovalTicket ticket = tickets.claim(ticketId, userId);
return new ApprovalResult(ticket, Instant.now());
}
}
高风险操作的链路是:
Agent 申请执行
│
▼
Gateway 拦截
│
▼
Policy 判定高风险
│
▼
生成审批凭证(此时什么都没发生)
│
▼
用户确认
│
▼
放行 → 执行 → 返回操作回执
注意中间的"生成审批凭证"——在用户确认之前,系统什么都没做。 这一步是安全性的全部意义:权限不是"提示一下",而是"结构上就做不到"。
四、执行要能断电续跑
回到开头那个场景。Agent 要跑五步:
搜索资料 → 生成文章 → 保存草稿 → 加入专题 → 发布文章
在"保存草稿"之后模型超时了。如果没有执行状态,系统很可能从头再跑一遍。后果是:
- 重复草稿、重复发布、重复扣费
- 数据状态不一致(草稿存了两份,专题只加了一半)
- 用户完全不知道完成到哪一步
生产系统要把任务拆成明确步骤,每一步的状态都落盘:
Step 1 检索资料 Completed
Step 2 生成内容 Completed
Step 3 保存草稿 Completed
Step 4 加入专题 Failed ← 修复后从这里继续
Step 5 发布 Pending
三个基本概念
幂等:同一个操作执行两次,结果不能错误地重复。
create_note(requestId = "abc123")
同一个 requestId 重试时,不再创建第二篇笔记。实现上用一个键值存储挡一道:
public class IdempotentInvoker {
private final IdempotencyStore store;
public <T> T call(String key, Supplier<T> action) {
Optional<IdempotencyRecord> hit = store.find(key);
if (hit.isPresent()) {
return hit.get().replayResult(); // 重放上次结果,不再执行
}
T result = action.get();
store.save(key, result);
return result;
}
}
业务侧只要带着 requestId 调用:
public ToolReceipt deleteNote(String requestId, String noteId) {
return idem.call("delete-note:" + requestId,
() -> noteRepository.deleteAndRecycle(noteId));
}
Checkpoint:每完成一步,保存当前状态。
public class AgentRun {
private final String runId;
private final RunStatus status; // PENDING / RUNNING / PAUSED / FAILED / COMPLETED
private int cursor; // 当前执行到第几步
private final List<TaskStep> steps;
}
public record TaskStep(
String stepId,
StepType type, // RETRIEVE / GENERATE / SAVE_DRAFT / PUBLISH
StepStatus status, // COMPLETED / FAILED / PENDING
Checkpoint checkpoint // 这一步完成后的状态快照
) {}
Resume:发生超时、模型故障或服务器重启后,从最后一个检查点继续。
public class RunResumer {
public AgentRun resume(String runId) {
AgentRun run = store.load(runId);
List<TaskStep> pending = run.steps().stream()
.filter(s -> s.status() != StepStatus.COMPLETED)
.toList();
for (TaskStep step : pending) {
StepResult result = executor.execute(step);
store.saveCheckpoint(run, step, result); // 每步落盘
if (!result.success()) {
throw new StepFailed(step, result, retryPolicy.nextDelay());
}
}
return run;
}
}
重试本身要受控,不能无限循环烧钱:
public record RetryPolicy(
int maxAttempts,
Duration backoff,
Class<? extends Throwable>[] retryOn // 只重试可恢复的异常
) {}
这条链路的形状:
任务开始
│
▼
建 AgentRun(记录锚点)
│
▼
Step 1 → 落 Checkpoint 1 → 完成
│
▼
Step 2 → 落 Checkpoint 2 → 完成
│
▼
Step 3 ── 超时 ✗
│
▼
Resume:从 Checkpoint 2 继续,只重跑 Step 3 以后
│
▼
幂等键兜底:Step 3 重试时不重复建草稿
五、Receipt:每个操作都要有回执
Agent 请求"删除笔记 123",执行完不能只返回一个 success。
success 是无状态的、无法核验的。它回答不了:是谁删的?什么时候删的?删了能不能恢复?是不是重复执行?
要返回结构化回执(Receipt):
{
"operationId": "op-789",
"tool": "delete_note",
"resourceId": "123",
"status": "completed",
"executedAt": "2026-08-04T10:20:00+08:00",
"actor": "user-001",
"recoverable": true
}
public record ToolReceipt(
String operationId, // op-789,全局唯一
String tool, // delete_note
String resourceId, // 123
OperationStatus status, // COMPLETED
Instant executedAt,
String actor, // user-001
boolean recoverable // 是否支持撤销/恢复
) {}
它的用途:
- 判断操作是否真的完成
- 靠
operationId防重复执行 - 支持审计(谁在什么时候做了什么)
- 支持撤销或恢复
- 排查 Agent 为什么得到某个结果
一个具体的例子:删除进垃圾箱
你的系统里,删除笔记后进入垃圾箱。这时要把回执保留下来:
删除回执(op-789,note 123 → trash)
恢复回执(op-791,note 123 ← trash)
标签关联变化
索引更新状态
为什么需要这些?因为跨模块的不一致才是真正的坑。看这个事故:
public class RecycleBin {
// 删除 = 移入垃圾箱 + 落一条回执
public ToolReceipt delete(String userId, String noteId) {
Note moved = noteRepo.moveToTrash(noteId, userId);
vectorIndex.softDelete(noteId); // 同步降权向量索引
return new ToolReceipt(opId(), "delete_note", noteId,
OperationStatus.COMPLETED, now(), userId, true);
}
// 恢复时带上原回执的 operationId,天然幂等
public ToolReceipt restore(String operationId) {
ToolReceipt origin = receiptStore.load(operationId);
Note back = noteRepo.restoreFromTrash(origin.resourceId());
vectorIndex.reindex(origin.resourceId()); // 索引一起恢复
return new ToolReceipt(opId(), "restore_note", origin.resourceId(),
OperationStatus.COMPLETED, now(), origin.actor(), false);
}
}
如果没有这条回执链,会出现最恶心的情况:页面显示删除,但向量索引里还躺着这篇笔记——用户搜到一篇已删除的文章,点进去 404。有了回执,"删除"和"索引更新"这两个动作就有了同一个锚点,可以一起完成、一起补偿。
这条链路的形状:
工具调用
│
▼
执行业务(移入垃圾箱)
│
▼
同步副作用(降权向量索引)
│
▼
落 Receipt(operationId / actor / 时间 / 可恢复性)
│
▼
返回给 Agent / 审计 / 幂等键 / 撤销凭证
六、Replay:问"为什么"的时候,能还原现场
用户一句"AI 为什么得出这个结论",是整个系统最难回答的问题。因为模型是概率性的——同一段输入,今天和明天的输出可能不一样。如果你不把现场存下来,你只能两手一摊。
Replay 的前提是把一次执行完整保存下来:
使用的模型和版本
系统提示词版本
用户输入
Context Snapshot(模型当时看到的一切)
检索到的文档
工具调用请求
工具执行结果
最终回答
Token、延迟和费用
public record AgentTrace(
String traceId,
String modelId, // 哪个模型、哪个版本
String promptVersion, // 提示词版本
String userInput,
ContextSnapshot snapshot, // 当时喂给模型的完整上下文
List<ToolCallRecord> toolCalls, // 每个请求 + 对应回执
String finalAnswer,
CostMetrics cost // token / 延迟 / 费用
) {}
public class ReplayService {
public ReplayResult replay(String traceId) {
AgentTrace trace = traceStore.load(traceId);
CompiledContext ctx = snapshotStore.restore(trace.snapshotVersion());
// 用当时的上下文,跑现在的模型 / 提示词
return runner.run(ctx, trace.modelId());
}
}
Replay 的三个用处
排查问题。先还原现场,再判断到底是哪一环:
- 检索错了?
- 上下文过期了?
- 提示词有问题?
- 模型判断错了?
- 工具返回了错误?
模型升级回归测试。模型从 A 换成 B,把历史任务重新跑一遍,对比:
指标 模型 A 模型 B
准确率 92% 88%
工具调用成功率 98% 95%
P50 延迟 1.8s 2.1s
单次成本 0.032 0.041
这样"换模型"从"凭感觉拍板"变成"看数据做决定"。
建立评估集。从真实失败案例里挑任务,固定成测试集。以后改了提示词、改了 RAG、改了流程,不是靠感觉说"好像变好了",而是跑一遍回归测试看数字。
这条链路的形状:
一次执行全程落 Trace
│
▼
用户问"为什么"
│
▼
按 traceId 找回快照 → 还原当时上下文
│
▼
重新执行 / 对比新旧模型 / 跑评估集
七、经验要过审,才能成为规则
假设一次 Agent 执行成功了:
用户问 Java 问题 → 搜索前三篇文档 → 成功回答
能不能因此自动生成一条永久规则:“以后所有问题只搜索前三篇文档”?
不能。一次成功可能只是偶然。 更危险的是另一种做法——系统自己根据少数案例偷偷改变行为。一旦它开始这么干,你解释不了系统为什么变了,也没法回退。
正确流程是把"线上经验"变成"正式策略"之间,加一道完整的晋升管道:
线上执行
│
▼
保存 Trace
│
▼
形成候选案例
│
▼
离线评估 ← 数据说话,不靠感觉
│
▼
人工审核 ← 高风险的经验,人必须过目
│
▼
确认有效
│
▼
灰度发布(先小流量)
│
▼
升级 Prompt、规则或检索策略
这叫做 Promotion:只有经过评估的经验才能晋升为正式策略。它保证了系统行为是可解释的、可回退的、可审计的。
public class PromotionPipeline {
private final TraceStore traces;
private final EvaluationService eval;
private final HumanReviewQueue reviews;
private final RolloutService rollout;
// 线上 Trace 进来,够格的进人工审核队列
public void ingest(AgentTrace trace) {
if (eval.isCandidate(trace)) {
reviews.submit(trace);
}
}
// 审核通过,灰度发布,观察后全量
public void promote(String reviewId, StrategyPatch patch) {
if (reviews.approved(reviewId)) {
rollout.apply(patch, canaryPercent = 5);
}
}
}
这条链路的形状:
线上执行 → 落 Trace → 候选案例库 → 离线评估 → 人工审核
│ 通过
▼
灰度发布 → 观察指标 → 晋升为正式策略
八、落地:先立边界,再加恢复,最后上治理
不需要一次性把整个 Agent 平台搭出来。分三阶段,每一阶段都有明确的边界。
MVP:先立四个边界
AIApplicationService
ContextBuilder
LLMGateway
ToolGateway
第一条链路:
Controller
│
▼
AIApplicationService
│
▼
ContextBuilder
│
▼
LLMGateway
│
▼
模型供应商
Agent 要调用业务能力时,走第二条链路:
Agent
│
▼
ToolGateway
│
▼
Application Use Case
│
▼
Domain
这两条链路共同的红线是——Agent 不直接:
- 调 Repository
- 操作数据库
- 调用领域实体
- 访问模型 SDK
- 绕过权限系统
Phase 2:加恢复和可观测
AgentRun
ToolCallLog
ContextSnapshot
RetryPolicy
Timeout
IdempotencyKey
这一批解决的是第四、五节的问题:执行断了能续、重复了能挡、出了事能查。
Phase 3:上治理
Approval Workflow
Replay
Evaluation Dataset
Prompt Versioning
Model Comparison
Policy Engine
到这一步,才接近真正的生产级 Agent Runtime:审批、回放、评估、策略引擎全都在位。
把链路闭合
把每一节的局部链路拼回去,就是开头那张总领图的完整版:
用户目标
│
▼
AIApplicationService
├─ 建 AgentRun(记录与恢复的锚点)
│
├─ ContextBuilder 编译上下文
│ 身份 → 任务类型 → 检索 → 去重 → 排序 → 过滤 → Token 预算
│
├─ LLMGateway 取模型的"建议"
│ 建议里的工具调用 → ToolGateway
│ → PolicyEngine:权限 / 风险 / 审批 / Schema / 限额
│ → 通过 → UseCase → Domain → DB
│ → 返回 Receipt(可审计、可幂等、可撤销)
│
├─ 每步落 Checkpoint → AgentRun
│ 失败 → 从断点 Resume(幂等重试兜底)
│
└─ 全程落 Trace → Replay(排查/回归/评估集)
│
▼
Promotion 回路
线上经验 → 评估 → 审核 → 灰度 → 晋升为策略
回到最开始的那句话:
把不确定的智能,限制在确定的软件工程边界里。
LLM 可以不稳定,但外围系统必须稳定。各自的职责分得很干净:
模型 负责建议
策略 负责授权
工具 负责执行
Runtime 负责记录与恢复
人 负责高风险决策
这也是 AI 系统和普通后端最大的区别:你不能假设模型每次都会做对,所以架构要围绕"它可能判断错误"来设计。 模型出错不可怕,可怕的是出错之后系统既不能发现、也不能恢复、还不能解释。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)