一套可上线的 Agent Runtime,关键不在模型,而在可追踪、可验证、可修复的执行系统。
很多人看 Agent,第一眼会盯着模型:参数多大、上下文多长、推理强不强。工程里真正麻烦的地方往往不在这里。用户只发出一句话,系统却要判断意图、补齐字段、选择执行模式、调用工具、整理证据、生成草稿、做校验,最后还要决定哪些信息值得沉淀。
一个可用的 Agent Runtime,本质上是在模型外面加了一套执行系统。模型负责推理和表达,Runtime 负责把一次概率生成变成一条可追踪、可回放、能重试的链路。没有这层工程约束,模型很容易在复杂任务里变成"看起来会做事的聊天框"。
下面用一个简单任务贯穿全文:
帮我整理上周几个项目会议的结论,生成一份给团队看的周报。
这句话看起来不复杂。真要让系统自动完成,它至少要知道时间范围、项目范围、读者是谁、最终格式是什么,哪些字段可以自动查,哪些字段不能猜。Agent 的工作从这里开始。
Runtime 总图:模型只是一段执行链路里的一个节点
Agent 不是"LLM + 工具列表"。更准确的拆法,是把它看成一条运行时管线:前面把自然语言编译成任务结构,中间用上下文和工具驱动执行,后面用证据和校验决定能不能交付。
图:Agent Runtime 从用户请求到校验收尾的完整执行链路
这张图把 Agent Runtime 拆成更适合工程讨论的视角:模型不是入口,也不是终点,它夹在编排、状态、证据和校验之间。
图:Prompt 进入 Runtime 后先被编译成可执行任务契约
Prompt Compiler:先把一句话编译成任务契约
Agent 收到用户请求后,第一步不该急着回答。更稳的做法是先做 Prompt Compiler:把自然语言转成一个带字段、约束和风险标记的任务契约。
对"整理上周几个项目会议"这类任务来说,系统要抽出 task_type、time_range、audience、output_format,还要标出哪些字段没有明确给出。例如项目列表可以通过日历和文档发现,但 owner、deadline、项目状态不能随口补。
{
"task_type": "weekly_report",
"slots": {
"time_range": "last_week",
"projects": "discover_from_tools",
"audience": "team",
"output_format": "weekly_report"
},
"missing_slots": ["project_list"],
"must_not_guess": ["owner", "deadline", "project_status"],
"confidence": 0.82,
"clarification": {
"needed": false,
"reason": "projects can be discovered from calendar and docs"
}
}
这里最关键的是"能不能猜"。低风险字段可以从默认偏好或上下文里补;高风险字段必须查证,查不到就标待确认。很多 Agent 失败,不是模型不会写,而是入口阶段没有把"禁止编造"的字段圈出来。
| 机制 | 运行时职责 | 输出 |
|---|---|---|
| 输入清洗 | 处理指代、隐私字段、噪声文本,必要时做 query rewrite | 更干净的用户请求 |
| 意图分类 | 判断当前是问答、写作、查询、操作还是混合任务 | task_type |
| 槽位填充 | 抽取时间、对象、格式、收件人、范围等关键字段 | slots |
| 缺口检测 | 按任务 schema 检查必填字段是否缺失 | missing_slots |
| 澄清策略 | 判断追问、自动补全、继续执行还是兜底 | clarification |
样例库在这一层也有用。系统可以把用户请求转成检索 query,找历史上相似任务的 task_type、槽位结构和路由结果,作为 few-shot 参考。这样模型不是凭感觉分类,而是在已有执行样例附近做判断。
Execution Router:先选执行模式,再绑定 Skill 和 Tool
意图识别之后,Runtime 要做一次路由决策。这里不只是"选哪个工具"。它要判断当前任务应该直接回答、追问、走 ReAct、走 Plan-Execute,还是进入安全兜底。
简单事实问题可以直接回答。目标明确、步骤稳定的任务适合 Plan-Execute。需要边查边判断、下一步强依赖工具返回的任务,适合 ReAct。高风险或字段缺失严重的任务,应该先问清楚,不要用聊天模式硬跑。
{
"decision": {
"route": "use_skill_and_tools",
"reasoning_mode": "plan_execute",
"selected_skill": "weekly-report",
"tool_candidates": ["calendar.search", "docs.search", "im.search"],
"plan": [
"find meetings from last week",
"collect project evidence",
"draft weekly report",
"verify claims"
],
"fallback": {
"if_low_confidence": "ask_clarification",
"if_tool_unavailable": "safe_execute_with_uncertainty"
}
}
}
ReAct 和 Plan-Execute 的差别,可以用下面这张图看清楚:
图:ReAct 与 Plan-Execute 在执行节奏上的差异
工程上最好不要让模型在全量 Skill 和 Tool 里自由挑。先由 Runtime 根据任务类型、风险和依赖关系筛出候选,再把候选能力注入上下文。能力列表越大,越需要路由层,不能把"在一千个工具里选一个"的压力全甩给模型。
| 决策问题 | 更稳的处理方式 |
|---|---|
| 什么时候用 ReAct | 下一步依赖工具观察结果,或者资料状态不确定 |
| 什么时候用 Plan-Execute | 目标清楚、步骤可拆、依赖关系比较稳定 |
| 什么时候追问 | 缺失字段会直接影响结果或带来风险 |
| 什么时候兜底 | 工具不可用、权限不足、风险超过阈值 |
| 怎么避免无限循环 | 设置最大轮次、工具预算、重复观察退出条件 |
图:上下文运行时把稳定规则与任务现场分层管理
Context Runtime:静态前缀管规则,动态状态管现场
模型每一轮看到的,不是系统里所有信息。Runtime 会把上下文分成两类:静态前缀和动态状态。
静态前缀放稳定材料:系统规则、工具 schema、Skill 说明、输出格式、长期项目约定。动态状态放当前任务现场:已确认事实、未决问题、工具结果摘要、下一步目标。两者混在一起,模型会被噪声拖垮,缓存也很难命中。
{
"system\_rules": [
"do not fabricate facts",
"mark owner or deadline as pending if unsupported"
],
"tool\_schemas": [
"calendar.search(range, keyword)",
"docs.search(query, project)",
"im.search(query, time\_range)"
],
"skill": {
"name": "weekly-report",
"sections": ["结论", "进展", "风险", "下周计划"]
},
"project\_rules": {
"tone": "concise",
"must\_trace\_sources": true
}
}
这里有两个容易混淆的缓存概念。KV Cache 是单次生成内部的缓存,避免每生成一个 token 都重新计算历史上下文。Prompt Cache 复用的是跨请求的稳定前缀,比如系统规则和工具定义。前者提升 decode 阶段效率,后者降低重复前缀的计算成本。
| 缓存对象 | 复用范围 | 典型内容 | 主要收益 |
|---|---|---|---|
| KV Cache | 单次生成内部 | 已处理 token 的 Key/Value | 降低逐 token 解码开销 |
| Prompt Cache | 多次请求之间 | 系统规则、工具 schema、固定输出模板 | 减少稳定前缀重复计算 |
动态模板要特别小心。只要前缀里混入 {{user_name}}、当前日期、临时工具结果,原本可复用的稳定部分就可能变得不稳定。更好的做法是把固定规则和变化字段拆开,稳定前缀尽量稳定,变化字段放到动态后缀。
Observation Loop:工具结果不能原样塞回模型
Agent 调工具后,最忌讳把原始返回全文直接塞回下一轮。工具返回的是 observation,不是下一轮 prompt。Runtime 要做的是抽取结构化事实、更新 working state,再把下一轮要解决的问题交给模型。
一个任务跑到中途,状态里至少应该有这些内容:任务目标、执行到哪一步、已确认槽位、事实列表、实体状态、未决问题、工具历史、证据索引和质量标记。
{
"task": {
"task_id": "task_20260624_001",
"task_type": "weekly_report",
"user_goal": "整理上周几个项目会议的结论,生成一份给团队看的周报",
"status": "in_progress"
},
"execution": {
"reasoning_mode": "plan_execute",
"current_step": "collect_project_evidence",
"loop_count": 3,
"retry_count": 0,
"next_action": "retrieve_more_evidence"
},
"slots": {
"time_range": {
"value": "last_week",
"status": "confirmed",
"source": "user_prompt"
},
"projects": {
"value": ["A", "B", "C"],
"status": "confirmed",
"source": "calendar.search"
}
},
"open_questions": [
{
"field": "deadline",
"project": "A",
"reason": "周报需要 deadline,但当前证据中没有明确日期",
"priority": "medium",
"next_probe": {
"tool": "im.search",
"query": "A 项目 deadline"
}
}
],
"quality_flags": {
"has_missing_required_fields": true,
"has_conflicts": false,
"needs_more_retrieval": true,
"safe_to_generate_draft": true
}
}
这份状态不是越详细越好。它应该像飞行仪表盘,只保留继续执行需要的信息。工具原文可以保留 source id,没必要长期塞在 prompt 里。否则循环几轮之后,上下文会变成日志垃圾场,模型很难判断哪些是事实,哪些只是历史噪声。
图:工具返回结果进入工作状态的处理过程
同一事实多次返回不一致时,不要直接覆盖。状态里应保留来源、时间、权威性和置信度;新版本优先,正式来源优先,冲突内容进入待确认列表。
Evidence Pipeline:先召回一批,再留下能支撑答案的证据
生成之前,Runtime 还需要做证据筛选。RAG 不是"搜到什么塞什么"。更合理的链路是:query rewrite、混合召回、元数据过滤、reranker 重排、证据裁剪,最后输出一个能支撑关键断言的 evidence bundle。
图:证据从召回到裁剪的质量控制链路
Reranker 不应该只看 embedding 相似度。对周报任务来说,是否包含 owner、deadline、风险、决策结论,来源是不是正式会议纪要,更新时间是不是最新,都要参与排序。政策、合规、生产操作这类场景还要提高权威来源的权重。
{
"evidence_bundle": [
{
"claim_target": "A 项目优先做 MVP",
"source_type": "meeting_note",
"authority": "official",
"freshness": "high",
"rank": 1
}
],
"dropped_items": ["无关闲聊", "重复讨论", "过时草稿"],
"memory_summary": {
"confirmed_facts": 6,
"open_questions": ["B owner", "C deadline"]
}
}
证据裁剪的目标不是让上下文更短,而是让模型只看到"能支撑当前回答"的材料。旧版本文档、重复聊天、情绪化闲聊、没有 source id 的转述,都应该被降权或丢弃。
| 证据处理环节 | 主要风险 | 工程处理 |
|---|---|---|
| 召回 | 漏掉关键来源 | 多路检索,query rewrite,适当放宽召回 |
| 重排 | 语义相似但不权威 | 加入来源权威性、新鲜度、字段覆盖度 |
| 裁剪 | 上下文被噪声挤满 | 去重,压缩,按 claim 保留材料 |
| 冲突处理 | 新旧结论互相打架 | 同时保留冲突来源,标待确认 |
Draft Generator:生成的是草稿,不是最终答案
到了生成阶段,模型拿到的已经不是用户原始 prompt,而是任务契约、静态规则、动态状态和证据集合。好的生成要受约束:输出结构要固定,关键断言要有来源,不确定字段要显式标出来。
{
"draft_answer": {
"summary": "上周 A/B/C 三个项目均有明确进展",
"sections": [
{
"name": "结论",
"content": "..."
},
{
"name": "风险",
"content": "..."
}
],
"uncertain_items": ["B owner", "C deadline"],
"citations": ["meeting_note_2026_06_21", "project_doc_A"]
}
}
这里最好把生成产物叫 draft。这个命名很重要,它会提醒系统后面还有校验。很多事故都发生在"模型写完就直接返回"的链路里:语言很顺,但里面混进了无证据 owner、错误 deadline 或旧版本状态。
生成阶段常见的工程约束包括:
| 机制 | 作用 |
|---|---|
| Prefill | 让模型先吸收静态前缀、动态状态和证据集合 |
| Decode | 按 token 连续生成正文 |
| Stream Generation | 分片返回,支持取消、中断和超时处理 |
| Format Constrained Generation | 用 schema、固定标签或模板减少格式漂移 |
表达可以润色,事实不能自由发挥。证据里没有的结论,不要靠"合理推断"补上;缺字段就写待确认。
图: 按关键断言检查证据支持,再决定保留、补查或改写
Verifier:按 claim 检查,不要只给整段答案打分
草稿生成后,Runtime 要做验证。验证不是看文章顺不顺,而是看有没有漏字段、有没有编造、有没有和证据冲突。比较稳的办法是 claim extraction + evidence linking:把草稿拆成最小关键断言,再逐条找证据支持。
{
"verification_result": {
"coverage_ok": false,
"unsupported_claims": ["B 项目 owner 是李四"],
"missing_fields": ["C 项目 deadline"],
"contradictions": [
{
"claim": "A 项目风险解除",
"evidence": "会议纪要里仍标记为高风险"
}
],
"next_action": "retry_with_more_evidence"
}
}
claim 不需要切得过碎。结论、风险、负责人、截止日期、数值、状态变化要查;修辞性句子不用查。证据不足和证据矛盾也要分开处理:找不到证据是 insufficient,证据明确反向支持是 contradiction。前者可以补查或标不确定,后者必须改写或阻断。
图:草稿按关键断言进行证据校验与修复分流
校验还包括格式、敏感信息和内容安全。比如输出结构不合法时直接修格式;存在未脱敏人名、手机号、邮箱时阻断;高危操作建议要进人工确认或安全降级。
Repair Loop:校验失败后要补查、改写或降级
校验没过,不等于整个任务失败。Runtime 应该把问题映射成具体修正动作:缺证据就补查,表达不合规就改写,字段不确定就标待确认,工具失败就走重试策略。关键是别无脑重生整篇,这样很可能把原来的问题换个说法再写一遍。
{
"finalization": {
"status": "retry_required",
"reason": "missing evidence for B owner and C deadline",
"retry_plan": [
{
"action": "retrieve_more",
"source": "im.search",
"target": "B owner"
},
{
"action": "revise_answer",
"mode": "mark_uncertain"
}
],
"user_visible_output": null
}
}
重试必须有预算。比如最多补查两次、改写一次;再失败就进入 fail-safe,说明缺口和可用信息,不要一直循环。LangChain 多代理流程陷入长时间循环、工具费用失控,根源往往就是没有把退出条件和预算做成硬约束。
| 失败类型 | 修复动作 |
|---|---|
| 工具超时 | 按工具类型重试,超过次数后降级 |
| 证据不足 | 针对缺口字段二次检索 |
| 证据冲突 | 保留冲突来源,删除确定性结论 |
| 格式错误 | 走结构化修复,不重跑全部任务 |
| 高风险不确定 | 请求用户确认或转人工 |
Closure:答案发出后,任务还没真正结束
Agent 返回最终答案之后,还需要做收尾。这里的核心问题是:哪些信息可以进入长期记忆,哪些只属于本次任务现场,哪些日志要保留给评估和排障。
{
"task_closure": {
"status": "completed",
"long_term_memory_candidates": [
{
"type": "user_preference",
"content": "团队周报偏好结论先行、风险单独列出",
"write_back": true,
"reason": "stable and reusable"
}
],
"short_term_memory_cleanup": [
"raw_tool_results",
"temporary_search_candidates",
"intermediate_drafts"
],
"evaluation_log": {
"tools_used": ["calendar.search", "docs.search"],
"retry_count": 1,
"verification_passed": true
}
}
}
长期记忆不能变成垃圾桶。稳定偏好、项目固定规则、用户明确确认的信息可以写入;一次性会议结论、临时搜索候选、原始工具日志不应该长期保存。记忆还要带来源、时间戳和版本,新信息和旧记忆冲突时先进入候选区,别直接覆盖。
短期记忆和长期记忆的职责可以这样分:
| 类型 | 保存内容 | 读写方式 | 退出策略 |
|---|---|---|---|
| 短期记忆 | 当前任务状态、最近工具结果摘要、未决问题 | 每轮读取和更新 | 任务结束后压缩或清理 |
| 长期记忆 | 稳定偏好、项目规则、可复用背景 | 新任务开始时读取,确认后低频写入 | 被新版本覆盖、长期不用或用户否认时降权 |
任务日志也要保留到合适粒度。工具调用、重试次数、失败原因、验证结果值得保存;原始敏感内容、无关中间草稿、可还原隐私的 trace 不该长期留着。
结语:好 Agent 靠 Runtime 托底
Agent 的难点不在"让模型说得像会做事",而在把一次请求拆成可执行、可检查、可回滚的链路。Prompt Compiler 负责入口契约,Execution Router 负责路径选择,Context Runtime 负责状态,Evidence Pipeline 负责事实质量,Verifier 和 Repair Loop 负责交付边界,Closure 负责把经验留下来。
模型越强,这套 Runtime 越重要。因为强模型更容易让人忽略工程边界,直到它在高风险任务里自信地编出一个看似合理的答案。真正能上线的 Agent,不是每次都像天才一样一次答对,而是知道什么时候该查证、什么时候该停、什么时候该承认不确定。
推荐阅读
Agent 评测系统架构:从指标分层到 GT/Judge 闭环的工程化落地
Agent 工程化新底座:用 CLI 契约层打通 HTTP 接口与业务能力