Agent 开发范式:自动化、可控性、可追溯、可干预的工程平衡
基于博主自己开发的合同资产底稿智能体的实践经验,提炼为通用 Agent 开发范式。
核心命题:如何在自动化、可控性、可追溯、可干预之间找到工程上最优的平衡点。
一、核心认知框架
1.1 Agent 的工程本质
agent = model(生产能力) + harness(工具·约束·评估·重试) + memory(跨轮次的状态和知识)
大模型的智力是够的,关键在于 harness 的发挥。就像人会遗忘无关的内容,让语言模型达到工作记忆、长期记忆的效果即可。
Harness 就是把不确定变成工程上可控的不确定:
- 锁定可控因素:固定 seed、temperature、system prompt
- 记录重现路径:保存每一次生成的完整上下文
- 重试+投票:多次生成,取多数结果
- 可验证中间态:把黑盒决策变成可检查的步骤
大模型的可信,不是靠模型自己,而是靠围绕它的工程系统。
1.2 不确定性的本质
大模型预测生成,本质是 next token prediction。它没有独立立场,只有"上下文延续"的目标。这意味着:
- 每次模型的输出都是随机的预测生成
- 单纯 SOP 不可重放,结果会飘移
- 智力的一部分来自幻觉------新的组合来自概率引擎的随机性
因此 harness 要允许智能体试错、检验,直到通过标准。技术路径是在特定时机抽出结果来检验,减少有害幻觉,提高有效智力。
1.3 行业智能体 vs 通用智能体
通用模型可以学 SOP,但行业定制智能体的不可替代性在于:
| 维度 | 通用模型能做 | 定制智能体必须做 |
|---|---|---|
| 流程理解 | 能学 SOP | 模板保真 + 异常分级 |
| 数据处理 | 能处理标准格式 | 异构表格结构标准化 |
| 决策质量 | 概率置信 | 状态机 + 规则复核 |
| 结果交付 | 自由输出 | 结果可追溯 + 版本管理 |
| 失败处理 | 重试或放弃 | 分级降级 + 工程责任链 |
强化定制智能体的核心价值:模板保真 + 异常分级 + 状态机 + 结果可追溯------这套工程责任链是大模型自身能力演进无法直接覆盖的。
二、LLM 裁决节点设计范式
2.1 核心原则:候选输出 + 规则复核 + 可降级
避免自由式 agent 破坏可控性。LLM 根据规则和契约裁定,而非自由决策。
规则定位 → [成功] → 直接采用
↓ [失败]
触发 LLM 候选 → 规则验证 → [通过] → 采用
↓ [仍失败]
换一种探索策略(扩大表头采样范围 / 变换匹配维度)→ 再试
↓ [仍失败]
降级(标记 REVIEW / 人工复核)
2.2 inferWithFeedback 标准模式
async inferWithFeedback<T>(
initialContext: unknown,
llmMethod: (context: unknown) => Promise<T>,
validate: (result: T) => { ok: boolean; feedback?: string },
maxAttempts: number = 2
): Promise<T | null> {
let context = initialContext
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const result = await llmMethod(context)
const validation = validate(result)
if (validation.ok) return result
context = { ...initialContext, previousAttemptFeedback: validation.feedback }
}
return null // 降级到规则兜底或人工复核
}
关键约束:
- 不改变固定管线架构
- 最多 2-3 次 LLM 调用(不是无限循环)
- 失败后仍然降级到规则兜底
- 每个高歧义节点独立使用,不串成跨节点的自由循环
2.3 LLM 节点的输入输出契约
每个 LLM 判断节点必须具备完整契约:
┌─────────────────────────────────────────────────┐
│ LLM 判断节点契约 │
├─────────────────────────────────────────────────┤
│ 1. System Prompt 约束(禁止越权语义) │
│ 2. User Payload 结构(可序列化、可快照) │
│ 3. Schema 返回结构(Zod 校验) │
│ 4. Fail Code 分支(每种失败有对应码) │
│ 5. 降级策略(失败时退回规则或人工) │
│ 6. 日志要求(输入规模/模型参数/校验结果/采纳态) │
└─────────────────────────────────────────────────┘
2.4 禁止 LLM 越权的硬约束
LLM 不可直接参与:
- 最终金额口径裁定
- 关键数值的最终写回决定
- 状态机判定(DONE / REVIEW / PARTIAL)
- 模板结构、公式区、宏的修改决策
- 跨节点的自由编排调度
LLM 只允许:
- 输出结构化候选(排序、置信度、理由)
- 语义理解辅助(表头识别、名称消歧)
- 说明性文本草稿生成
- 标记
needsHumanReview提示规则层
三、表格异构与结构标准化
3.1 处理范式:先结构标准化,再字段定位
原始表格 → 结构标准化 → 字段定位 → 数据抽取
│ │ │
│ 合并单元格展开 列映射推断
│ 多行表头合并 数据起始行识别
│ 空行/装饰行过滤 类型推断
│
└── 异构识别(表头矩阵、采样行、关键词命中)
3.2 列定位冲突时的降级策略
定位冲突时宁可降级,也不盲写:
Level 1: 坐标锚点定位(列号硬编码,适用于标准模板)
↓ [列号命中但表头不符]
Level 2: 表头锚点定位(关键词匹配)
↓ [多候选或无命中]
Level 3: LLM 语义定位(候选排序 + 规则复核)
↓ [规则复核失败]
Level 4: 降级 → 标记异常 + 人工复核
每一级的结果都要回馈下一级决策。
3.3 合并单元格处理
1. 读取时:展开为矩阵表达(每个逻辑单元格记录物理范围)
2. 表头识别:按层级拼接多行表头("借方" + "发生额" = "借方发生额")
3. 数据区定位:跳过装饰行,以首个全数值行为数据起始候选
4. 冲突处理:同一逻辑列有多个物理列候选时,输出候选清单而非强行选定
四、抽样输入与覆盖风险管理
4.1 问题本质
如果用户只给前后样本,中间异常可能遗漏。这不是模型能力能完全补掉的,必须在系统里显式暴露覆盖风险,并限制完成态。
4.2 设计原则
1. 显式暴露覆盖风险
- 任务结果必须标注"采样覆盖率"和"未覆盖区域"
- 输出中明确标记"以下区域未经验证"
2. 限制完成态
- 覆盖率不足时,禁止标记 DONE
- 部分覆盖 → REVIEW(需人工补充验证)
- 严重不足 → PARTIAL(半成品输出)
3. 异常检测策略
- 不仅检查已匹配项,还要主动发现"未被任何规则命中的项"
- 输出"未匹配残留项清单"作为覆盖风险指标
4. 增量验证接口
- 允许用户补充样本后重新运行局部链路
- 增量验证结果合并到已有结果集
4.3 覆盖率量化模型
interface CoverageReport {
totalItems: number
matchedItems: number
unmatchedItems: number
coverageRate: number // 0~1
uncoveredRegions: string[] // 未覆盖的区域描述
riskLevel: 'low' | 'medium' | 'high'
allowedStatus: ('DONE' | 'REVIEW' | 'PARTIAL')[] // 基于覆盖率限制可输出的状态
}
五、脏数据处理与鲁棒性验证
5.1 脏数据分类
| 脏数据类型 | 示例 | 处理策略 |
|---|---|---|
| 格式异常 | 金额列混入文本、日期格式不统一 | 类型检测 + 清洗规则 + 异常标记 |
| 结构异常 | 合并单元格打断数据区、隐藏行/列 | 结构标准化前置 |
| 语义异常 | 同名不同义、缩写混用、错别字 | 标准化 + 候选匹配 + LLM 消歧 |
| 缺失异常 | 关键字段为空、整列缺失 | 缺失检测 + 降级策略 + 待补标记 |
| 冲突异常 | 多源数据对同一字段给出不同值 | 优先级规则 + 冲突裁决 + 审计追踪 |
| 边界异常 | 超长文本、极大/极小数值、特殊字符 | 边界检查 + 截断/归一化 |
5.2 测试数据集设计原则
1. 正常流样本:标准格式、字段完整、无冲突
2. 边界样本:最小输入、最大输入、极端值
3. 故意脏数据样本:
- 表头错位(列号偏移 1-2 列)
- 名称变体(简称、别名、错别字各 3-5 例)
- 数据区插入空行/装饰行
- 合并单元格跨越数据区
- 金额列混入中文
- 同一项目在不同源中名称不一致
4. 半成品样本:缺少 1-2 个关键输入文件
5. 冲突样本:多源数据给出矛盾值
5.3 鲁棒性验证流水线
每次发布前必须通过:
1. 正常流全通过(基线)
2. 已知脏数据样本全降级到正确状态(不 crash、不盲写)
3. 新增脏数据样本至少覆盖上次失败的场景类型
4. 回归:新功能不破坏已有样本的处理结果
六、工具结果回灌与决策链路
6.1 核心思想:每一步工具结果回灌下一步决策
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Step N │────→│ Tool Call │────→│ Result N │
└──────────┘ └──────────┘ └──────────┘
│
▼
┌──────────────┐
│ Context for │
│ Step N+1 │
└──────────────┘
│
▼
┌────────────────────┐
│ Decision: Next Tool │
│ or Degrade or Done │
└────────────────────┘
实现要点:
- 工具返回结构化结果(不只是 success/fail,还有 what happened)
- 结果中包含"下一步可选动作"的 hint
- 失败结果中包含"失败原因分类",供决策层选择恢复策略
6.2 关键节点 Hook 系统
interface AgentHookSystem {
hooks: {
beforeLlmCall: Hook[] // 注入额外上下文、拦截超长输入
afterLlmCall: Hook[] // 审计日志、响应质量评分
beforeToolCall: Hook[] // 参数校验、权限检查
afterToolCall: Hook[] // 结果摘要、异常检测
beforeWrite: Hook[] // 最终校验、冲突检测
afterWrite: Hook[] // 死数扫描、一致性校验
onDegrade: Hook[] // 降级原因记录、通知
onComplete: Hook[] // 结果验证、覆盖率统计
}
}
Hook 的价值:
- 主循环可扩展,新增门禁不改 Service 主逻辑
- LLM 用在歧义分支点的语义判断和探索循环
- 在特定时机抽出结果来检验(过程审核)
6.3 歧义分支点的语义判断
识别歧义分支点 → Hook 拦截 → LLM 语义判断 → 规则验证
│
┌─────────┼─────────┐
▼ ▼ ▼
路径 A 路径 B 降级路径
(高置信) (中置信) (低置信/冲突)
LLM 在歧义分支点的角色是"探索循环":
- 给出候选路径
- 规则层验证可行性
- 如果不可行,换一种探索策略
- 如果多次不可行,降级
七、任务板与子任务摘要系统
7.1 从子任务摘要升级为任务板
interface TaskBoard {
taskId: string
overallStatus: 'RUNNING' | 'DONE' | 'REVIEW' | 'PARTIAL' | 'FAILED'
subtasks: SubtaskEntry[]
timeline: TimelineEvent[]
coverageReport: CoverageReport
issuesSummary: IssueSummary
}
interface SubtaskEntry {
id: string
name: string
status: 'pending' | 'running' | 'done' | 'skipped' | 'degraded' | 'failed'
input: string // 输入来源摘要
output: string // 输出结果摘要
skipReason?: string
degradeReason?: string
issues: Issue[]
duration?: number
llmCallCount?: number
}
interface TimelineEvent {
timestamp: number
subtaskId: string
event: 'start' | 'tool_call' | 'llm_call' | 'validation' | 'degrade' | 'complete' | 'error'
detail: string
}
7.2 任务板的用户价值
任务板提供:
1. 整体进度一目了然(哪些子任务完成/跳过/降级)
2. 异常定位(哪一步出了问题、为什么跳过)
3. 可干预点(用户可以对 REVIEW 状态的子任务补充信息后重跑)
4. 审计追踪(每个决策点的输入/输出/依据可追溯)
5. 成本可视化(LLM 调用次数、token 消耗)
7.3 可干预接口
interface TaskInterventionAPI {
// 用户补充信息后重跑某个子任务
retrySubtask(taskId: string, subtaskId: string, additionalContext?: unknown): Promise<SubtaskResult>
// 用户确认 LLM 候选结果
confirmCandidate(taskId: string, subtaskId: string, candidateId: string): Promise<void>
// 用户覆盖自动决策
overrideDecision(taskId: string, subtaskId: string, manualResult: unknown): Promise<void>
// 获取当前任务板快照
getTaskBoard(taskId: string): Promise<TaskBoard>
// 获取特定子任务的完整决策链路
getDecisionTrace(taskId: string, subtaskId: string): Promise<DecisionTrace>
}
八、可追溯性工程
8.1 可追溯的四个层次
Layer 1: 输入追溯
- 每个输出值可回溯到来源文件的具体单元格
- 数据流向图:源 → 中间态 → 最终输出
Layer 2: 决策追溯
- 每个 LLM 判断节点的输入快照、模型参数、结构化输出
- 规则复核结果(采纳/拒绝/降级)
- 降级原因分类
Layer 3: 变更追溯
- 每次写回的 diff(哪些单元格被写入了什么值)
- 公式保护日志(哪些公式被保留、哪些被覆盖)
- 版本树(v1 → v2 → v3,可回退)
Layer 4: 异常追溯
- 异常分级与异常码字典
- 每个异常的触发条件、影响范围、建议动作
- 异常收敛路径(异常 → 子任务状态 → 整体状态)
8.2 决策追溯的实现
interface DecisionTrace {
subtaskId: string
timestamp: number
decisionType: 'rule' | 'llm' | 'hybrid' | 'manual'
// 输入快照
inputSnapshot: {
sampleSize: number
keyFields: string[]
contextSummary: string
}
// LLM 相关(仅 llm/hybrid)
llmTrace?: {
model: string
temperature: number
promptHash: string // 不存完整 prompt,存 hash 可回查
candidateCount: number
selectedCandidate: number
confidence: number
reasons: string[]
}
// 规则复核
ruleValidation: {
passed: boolean
failedRules: string[]
feedback?: string
}
// 最终结论
outcome: 'accepted' | 'rejected' | 'degraded' | 'human_review'
outcomeReason: string
}
8.3 日志规范
必须记录(结构化日志):
1. 调用方法名 + 阶段标识
2. 输入样本规模(候选 sheet 数、候选行数、字段数)
3. 模型名、temperature 等关键参数
4. 校验结果(通过/失败/降级/人工复核)
5. 最终采纳态
禁止记录:
1. 完整敏感业务文本(用摘要/长度/截断片段替代)
2. 客户原始数据全文(用统计指标替代)
3. API 密钥或认证信息
九、错误恢复与降级策略
9.1 错误分类与恢复预算
| 错误类型 | 恢复策略 | 预算 | 降级路径 |
|---|---|---|---|
| LLM 输出被截断 | 续写(带已输出部分要求继续) | 最多 1 次 | 规则兜底 |
| 上下文超长 API 报错 | 压缩历史后重试 | 最多 1 次 | 摘要替换 |
| 网络抖动 | 退避重试(1s → 3s → 10s) | 最多 3 次 | 标记网络异常 |
| 列映射校验失败 | 带失败原因反馈重试 | 最多 2 次 | 规则兜底 + REVIEW |
| JSON 解析失败 | 尝试修复格式后重解析 | 最多 1 次 | 降级模板 |
| 工具调用异常 | 参数修正后重试 | 最多 2 次 | 跳过该步 + 异常记录 |
| 模型置信度高但结果错误 | 规则复核拦截 + 换策略重试 | 最多 2 次 | 降级 + 人工复核 |
9.2 降级分级
Fatal(任务失败无产物):
- 核心模板损坏
- 必要前置依赖完全缺失且不可恢复
Partial-Warning(可导出半成品):
- 缺少关键输入文件(TB、合同资产汇总)
- 主链路部分子任务失败
Review-Warning(可导出但需复核):
- 项目匹配存在歧义
- LLM 候选与规则冲突
- 覆盖率不足
- 数据源冲突
9.3 "模型给了置信度高但错误的路由"的处理
这是实际开发中最隐蔽的问题。解决方案:
1. 永远不信任单一置信度
- LLM 的 confidence 是自我评估,不是客观指标
- 必须有独立的规则层验证
2. 对抗性验证
- 对高置信结果做"反向验证":如果这个结果正确,其他约束是否也成立?
- 例如:匹配置信度 0.95,但检查"该候选是否已被其他项目占用"
3. 多维度交叉验证
- 不仅看语义相似度,还要看结构位置、数量级、上下文一致性
- 任何单一维度的高置信都不够
4. 审计追踪
- 记录所有高置信但被规则拒绝的 case
- 积累失败模式库,反哺 prompt 和规则优化
十、上下文管理与记忆系统
10.1 上下文压缩策略
| 时机 | 压缩手段 | 保留什么 |
|---|---|---|
| 工具结果超过阈值时 | 大输出只保留摘要,全文写磁盘 | 关键统计 + 异常标记 |
| 同一工具第 N 次调用时 | 旧结果替换为占位 | "已于第 N 轮读取,结果见摘要" |
| 对话超过 N 轮时 | 整段历史总结为摘要 | 当前目标 + 已完成 + 关键决定 + 下一步 |
| 跨会话时 | 仅保留 memory 条目 | 客商别名、覆盖规则、项目约定 |
10.2 记忆系统设计
应进入记忆的内容(不可重新推导的知识):
- 客商别名:用户明确纠正过的映射
- 科目覆盖规则:用户指定的特殊映射
- 项目约定:函证阈值、排除规则
- 外部资源指针:历史文件路径
使用优先级:
- memory 作为方向提示
- 当前文件/配置/工具结果为准
- 如果 memory 与当前状态冲突,优先相信当前观察
增量开发的记忆衰减:使用频率下降时,逐步清空旧的知识,避免过期 memory 误导。
10.3 系统提示词分层架构
Layer 1: 核心身份(固定)
"你是审计助手,负责合同资产底稿编制。"
Layer 2: 工具列表(固定)
各工具的 schema 和调用约束
Layer 3: Memory(跨会话,可选)
"该客户上次纠正:'小马科技' = '小马快跑科技有限公司'"
Layer 4: 当前环境(每轮动态)
"当前结果文件:xxx.xlsm,42行12列,存在3处异常"
Layer 5: Reminder(条件追加)
"注意:上次匹配尝试因重名冲突失败,请注意单位字段"
十一、工程化 Agent Loop 的适用性判断
11.1 何时用 Agent Loop,何时用固定管线
判断维度:
1. 数据结构是否确定?
- 确定 → 固定管线
- 不确定 → 需要探索机制
2. 步骤顺序是否固定?
- 固定 → 固定管线编排
- 需要动态决策 → Agent Loop 编排
3. 单步失败后是否有明确恢复路径?
- 有 → 固定管线 + retry
- 需要换策略 → Agent Loop 的反馈机制
推荐混合策略:
固定管线编排 + 高歧义节点内嵌 Agent Loop 反馈
11.2 合同资产智能体的适用性分析
15 个子任务中:
- 11 个是纯确定性(固定管线)
- 3-4 个有实质性不确定性(嵌入 inferWithFeedback)
- 0 个需要自由式 Agent Loop
结论:固定管线 + 高歧义节点内嵌反馈,而非全链路 Agent Loop
11.3 不应引入 Agent Loop 的地方
- 子任务编排顺序:合同资产底稿编制就是固定流程
- 金额计算和写回:纯确定性操作
- 异常收集和状态判定:规则状态机
- 函证抽样和替代测试:纯规则逻辑
十二、评估与持续优化
12.1 评估驱动的正循环
执行 → 评估 → 错误分类 → 修正 spec/prompt/规则 → 再执行
│
┌───────┼───────┐
▼ ▼ ▼
逻辑 遗漏 幻觉
错误 错误 错误
│ │ │
▼ ▼ ▼
修正规则 补充约束 加强校验
错误类型分类:
- 逻辑错误:规则理解偏差 → 修正规则表达
- 遗漏错误:边界条件未覆盖 → 补充约束和测试
- 歧义错误:多解释空间 → 收紧 prompt 约束
- 幻觉错误:无中生有 → 加强输出校验和事实核查
12.2 把生成评估从"感觉"变成"可计算"
将输出结果分成数个命题 → 逐命题判断真伪 → 不达标就重来
评估指标:
1. 结构完整率:输出字段数 / 期望字段数
2. 数值准确率:正确数值 / 总数值
3. 匹配准确率:正确匹配 / 总匹配
4. 降级合理率:应降级且降级 / 应降级总数
5. 覆盖率:已处理项 / 总项
6. 异常捕获率:已识别异常 / 实际异常
12.3 对抗性测试设计
因为大模型"容易顺着话茬走",必须设计中立、对抗性或结构化提问:
1. 故意提供矛盾信息,看模型是否能识别并标记
2. 提供"看似合理但实际错误"的候选,看规则层是否能拦截
3. 提供边界 case,看是否能正确降级而非强行给出结果
4. 模拟"前几轮正确、第 N 轮偏移"场景,验证长对话稳定性
十三、技术路线演进
13.1 当前阶段:harness + 通用模型
at first we harness, and then, we will make a more suitable model
阶段 1: harness(当前)
→ 工具集 + 知识接入 + 约束层 + 评估与重试层
→ 获得验证和认可
阶段 2: harness + 微调
→ 用评估数据微调 prompt / 适配层
→ 降低 token 消耗,提高一致性
阶段 3: harness + 轻量垂直模型
→ 对高频判断节点用小模型替代
→ 成本和延迟优化
13.2 当前工程瓶颈的本质
理论瓶颈(Transformer 固有):
- 自回归架构的 O(n²) 复杂度
- 固定上下文窗口的硬约束
- 概率生成导致的不可确定性
工程瓶颈(正在解决的):
- 如何让有限的上下文包含最有效的信息(RAG、压缩、摘要)
- 如何用更少的 token 传递相同的信息(结构化输出、schema 约束)
- 如何在不重复传全文的情况下保持多轮一致性(记忆系统)
13.3 代码库知识索引
代码库也可以做知识索引(Graph RAG 思路):
- 模块之间的依赖关系 = 图的边
- 每个模块的职责摘要 = 节点属性
- 增量开发时只加载相关子图
- 使用频率下降时逐步清空旧节点
实现:设计文档 + 增量日志 + 按需加载
→ 不是每次全读代码
→ 而是读一遍设计文档,每次开发做增量日志
十四、开发检查清单
14.1 每个 LLM 判断节点的检查清单
- 是否有明确的 system prompt 约束(含越权禁止)
- 是否有结构化 user payload(可序列化、可快照)
- 是否有 Zod schema 校验
- 是否有 fail code 分支
- 是否有降级策略(失败时的具体退路)
- 是否有规则复核层(不盲信 LLM 输出)
- 是否有日志记录(输入规模、模型参数、校验结果、采纳态)
- 是否有 stub 测试(不依赖真实模型也能测试所有分支)
- 是否限制了最大重试次数(不是无限循环)
- 是否保证了失败不阻断主链路
14.2 每个子任务的检查清单
- 输入来源是否明确且可追溯
- 输出结构是否有类型定义
- 异常出口是否完备(不只是成功路径)
- 降级行为是否定义清楚
- 状态对整体任务的影响是否明确
- 是否有独立的单元测试覆盖
- 是否有脏数据样本测试
- 工具结果是否回馈下一步决策
14.3 每次发布的检查清单
- 正常流样本全通过
- 脏数据样本正确降级
- 回归测试无破坏
- 模板保真验证通过(strict 模式至少跑 1 组样本)
- 覆盖率报告生成
- 异常码字典更新
- 决策追溯日志可查询
- 任务板结果展示正确
- 共存测试通过(不影响其他智能体)
十五、从项目深度到简历叙事
15.1 真实问题与解决方案对照表
| 真实问题 | 根因分析 | 解决方案 | 验证方式 |
|---|---|---|---|
| 工具调用没全召回 | 上下文过长导致模型忽略部分工具 | 上下文压缩 + 工具列表精简 + 分阶段注入 | 工具覆盖率测试 |
| 多轮记忆在长对话里丢失 | 上下文窗口限制 + 信息密度下降 | 摘要替换 + 关键决策显式保留 + 任务板锚定 | 第 N 轮仍能引用第 1 轮决策 |
| 模型给了置信度高但错误的路由 | 模型自评不等于客观准确率 | 独立规则验证层 + 对抗性交叉验证 | 高置信错误路由的拦截率 |
| 表格异构导致列定位失败 | 合并单元格/多行表头/命名变体 | 结构标准化前置 + 三级回退定位 + LLM 候选 | 脏数据样本集通过率 |
| 半成品需求与完成态冲突 | 缺输入时系统不知道能做到什么程度 | 覆盖率量化 + 状态限制 + 显式暴露未覆盖区域 | PARTIAL 状态判定准确率 |
| LLM 判断与规则结果冲突 | 概率输出与确定性规则的设计差异 | 规则层拥有最终裁定权 + 冲突进异常清单 | 冲突场景自动化测试 |
15.2 项目深度的体现
不是"我用了 LangChain 做了一个 agent"
而是:
1. 约束场景:审计底稿编制------信息不完整、工具调用、多轮记忆、错误路由
2. 设计决策:固定管线 + 高歧义节点嵌入反馈------为什么不用全 Agent Loop
3. 工程挑战:
- 表格异构处理(合并单元格、多行表头)
- LLM 高置信错误的发现与拦截
- 半成品输出的覆盖风险显式化
- 工具结果回灌决策链路
4. 可验证成果:
- 脏数据测试集通过率
- 决策追溯链路完整性
- 降级行为正确率
- 模板保真门禁
十六、设计决策原则总结
- 不确定性在哪里,反馈机制就嵌在哪里------不为确定性逻辑引入不必要的 LLM 调用
- 规则优先,LLM 辅助------LLM 输出必须经过规则校验,校验不通过时降级而非盲信
- 反馈比重试更有价值------带上"上次为什么失败"的信息重试,比无脑重试效果好一个量级
- 成本与延迟是硬约束------每一轮 LLM 调用都有 token 成本和延迟,必须在可控范围内
- 可测试性不能牺牲------嵌入反馈机制的节点仍需可 mock、可断言、可独立测试
- 失败必须可降级------任何 LLM 增强节点失败,都不应阻断主链路
- 覆盖风险必须显式------不能因为系统没报错就认为结果完整
- 可追溯是底线------每个决策点的输入/输出/依据都要可回查
- 允许试错但限制代价------harness 要允许探索循环,但每次探索有预算上限
- 先软约束,再硬优化------先用 harness 证明可行,再考虑微调或垂直模型
附录 A:合同资产智能体 LLM 节点优化路径
基于本范式,合同资产智能体下一阶段的 LLM 优化路径:
当前状态:纯规则主链路已闭环,LLM 节点尚未接入
Phase 1: inferWithFeedback 基础设施
- 实现标准化的反馈重试模式
- 建立 LLM 调用的日志与追溯框架
- 建立 stub client 测试基线
Phase 2: 高歧义节点逐个接入
- inferTbNetRowCandidates(TB 净额行识别)
- inferProjectMatchCandidates(项目名消歧)
- inferWorkbookSheets(Sheet 定位回退增强)
- inferColumnMapping(列映射推断)
Phase 3: 说明性文本生成
- buildContractAssetNotes(异常摘要/编制说明)
- 固定模板降级保底
Phase 4: 评估与迭代
- 收集 LLM 节点的实际准确率
- 识别"规则够用不需要 LLM"的场景(移除不必要调用)
- 识别"LLM 帮助显著"的场景(强化 prompt 和校验)
附录 B:与学习路径的关联
| 学习主题 | 在本范式中的映射 |
|---|---|
| superpower / brainstorm | 问题发现与解决方案的发散式探索 |
| 纵横分析法 | 跨智能体的适用性分析(横向)+ 单智能体内的深度设计(纵向) |
| skill 可阅读重构 | 本文档本身即为"按可阅读方式重构"的范式整理 |
| Graph RAG / 知识索引 | 代码库知识索引 + 增量开发的记忆管理 |
| memorix 记忆系统 | 记忆衰减 + 增量日志 + 按需加载 |
| harness 方向论 | 第十三节技术路线演进 |
| 评估正循环 | 第十二节评估与持续优化 |
补充专题:智能体范式与 Harness 工程
货币资金智能体 --- 智能体范式与 Harness 工程深度解读
这是《学习文档》的进阶篇。如果说《学习文档》回答的是"它是怎么做的",这一篇回答的是 "为什么这样做、它教会了我们什么、把这套方法迁移到下一个智能体上需要注意什么"。
阅读对象:已经看过《学习文档》、准备亲手设计或改造一个 SDK 原生智能体的开发者。
第一部分 关键概念深度阐述
下面这 8 个概念是理解整个系统的"承重墙"。每一个都不只是一个名词,而是一种设计取舍。
1.1 Agent Loop ------ 智能体的"心跳"
是什么
Agent Loop 是 LLM 与工具之间的多轮自动循环:
┌─────────────────────────────────────────────────┐
│ while not done: │
│ 1. 把 system + messages 送给 LLM 流式生成 │
│ 2. 流里同时出 text / thinking / tool_use │
│ 3. 解析响应:有没有 tool_use? │
│ ├─ 没有 → 收尾(done) │
│ └─ 有 → executeTools() │
│ → 把 tool_result 追加到 messages │
│ → 回到第 1 步 │
└─────────────────────────────────────────────────┘
为什么这是"范式",而不是"实现"
传统编程是 "开发者写流程,程序按流程跑" ;Agent Loop 是 "开发者给工具,LLM 自己决定调用顺序"。这意味着:
- 你不再写
if 用户上传Excel → 解析 → 出报告的硬编码 - 你只需注册
read_table、bash_run、fs_write等工具,LLM 看到任务后自己规划:先read_table探查 → 再bash_run跑 Python → 最后fs_write出底稿 - 代码的复杂度从"流程编排"转移到了"工具设计"和"提示词设计"
实践经验
| 经验 | 说明 |
|---|---|
| 工具粒度要"原子化" | 一个工具只做一件事。read_table ≠ read_and_analyze_table。粒度太粗,LLM 失去组合能力;粒度太细,LLM 会调爆 |
| 每个工具必须有清晰的副作用边界 | attributes: { readonly: true, noEffect: true }(如 conclude)让 SDK 知道这个调用不需要走权限审批 |
| 工具返回值必须信息化 | 不要只返回 { ok: true }。要返回结构("找到 3 张表,分别是..."),让 LLM 下一步有依据 |
| 不要让工具"思考" | 工具就该机械执行。决策要留给 LLM,不要把 if-else 逻辑下沉到工具里------那就退回到了传统编程 |
1.2 invoke / Agent / Session / agentId ------ 四个相关但不同的概念
新人最容易搞混的就是这四个词。它们的关系:
Session(会话,前端概念)
├─ 多个 invoke(一次"用户发一句 → 智能体回完")
│ ↓
└─ 一个 Agent 实例(SDK 概念,agentId = "cce-${sessionId}")
├─ messages[](完整历史,跨 invoke 持久)
├─ template(系统提示词,每 invoke 浅克隆)
└─ Store(自动持久化)
| 概念 | 谁定义 | 生命周期 | 标识 |
|---|---|---|---|
| Session | 项目(前端) | 用户开关会话 | sessionId |
| invoke | 项目(编排层) | 一来一回 | 无独立 ID |
| Agent | SDK | 进程内可恢复 | agentId(本项目用 cce-${sessionId}) |
| LLM 调用 | SDK Agent Loop 内部 | 一轮(毫秒级) | 无 |
为什么 agentId 要加前缀
cce-${sessionId},加了 cce-(cash-and-cash-equivalents 缩写)前缀。原因:
- 同一个 session 可能切换不同类型的智能体。如果都用 sessionId 作 agentId,SDK Store 会把不同类型的对话混在一起,恢复时彻底乱套
- 加前缀等于给 Store 分桶,每种智能体一个桶
泛化经验 :任何使用 SDK Store 的智能体,都应该用
<agent-type>-<sessionId>格式做 agentId。这是 SDK 用法的隐性约定。
1.3 系统提示词的"静态段 / 动态段"模型
概念
把 system prompt 分成两层:
| 层 | 性质 | 来源 | 示例 |
|---|---|---|---|
| 静态段 | 不变 | 代码常量 | "你是货币资金审计智能体..." |
| 动态段 | 可变,运行时注入/替换 | 工具调用 + Runtime Map | <workDir>、<conclusion>、<loaded_skills> |
为什么必须分两层
如果只有静态段,LLM 永远不知道:
- 当前在哪个工作目录(环境信息)
- 之前几轮做了什么(任务记忆)
- 加载了什么领域知识(按需扩展)
如果一股脑全塞静态段,token 爆炸,且无法跨轮变更。
为什么动态段用 XML 标签
<conclusion>
任务情况:完成银行存款分析,下一步函证核对
</conclusion>
而不是 JSON 或 Markdown。原因:
- 正则替换简洁可靠 :
/<conclusion>[\s\S]*?<\/conclusion>\s*/ - LLM 对 XML 标签遵守度高(Anthropic 官方推荐)
- 与 SDK 的
<skills_system>注入风格一致 - 可嵌套 :
<loaded_skills><skill name="x">...</skill></loaded_skills>自然成立
实践要点
| 要点 | 说明 |
|---|---|
| 每个动态段一个标签 | 不要混用,便于独立替换 |
| 替换前先正则探测 | 找得到就替换,找不到就追加。两条路径都要写 |
| 跨 invoke 持久 → Runtime Map | 不要塞 SDK Store;Store 是给 messages 的,不是给 system 的 |
| 跨进程持久 → 落盘 | 比如 vision_subagent 的 progress.json |
| session 隔离 → 每实例克隆 template | 详见下一节 |
1.4 Template Registry 与浅克隆陷阱
陷阱场景
// ❌ 直接修改共享模板
agent.template.systemPrompt += '<conclusion>...</conclusion>'
这一行代码看起来人畜无害,实际上会引发跨 session 数据污染:
Runtime(单例)
└─ TemplateRegistry(单例)
└─ template { systemPrompt: "..." } ← 共享对象!
↑
↑ resumeOrCreate 内部 this.template = registry.get(...)
↑
Agent A.template === Agent B.template ← 同一个引用
Session A 的 conclude 工具改了 systemPrompt,Session B 也跟着变了。客户的银行流水信息可能就这样泄漏到隔壁项目里去。
正确解法:浅克隆 + Runtime Map
// ✅ 在 invoke 开始时切断引用
const sharedTemplate = agent.template
agent.template = { ...sharedTemplate } // 浅克隆产生新对象
// 然后从 Runtime Map 恢复 session 自己的动态段
const stored = this.sessionConclusions.get(agentId)
if (stored) {
agent.template.systemPrompt += `<conclusion>${stored}</conclusion>`
}
为什么浅克隆够用?
AgentTemplateDefinition 是扁平对象:
{
id: string, // 不可变
systemPrompt: string, // ← 字符串是值类型,赋值=替换引用
tools: string[] | '*', // 数组,但本智能体不修改
permission: ...
}
浅克隆 { ...x } 创建新对象,赋值 clone.systemPrompt = '...' 不会触及原对象。所以只要不修改嵌套字段,浅克隆就够了。
泛化经验
任何"由工具修改 Agent 状态"的设计,都要先问一句:"这个状态是 Agent 实例独有,还是 Runtime 单例共享?" 共享对象上的修改一定要先克隆。
1.5 Skill ------ "按需加载的领域知识包"
与"系统提示词里直接写 SOP"的对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 静态写在 system prompt | 简单,每次都看得到 | token 爆炸;不同任务用不同 SOP 时浪费 |
| Skill 按需加载 | 元数据预注入,全文按需取 | 实现复杂;LLM 可能忘记加载 |
货币资金智能体选的是后者。原因是审计场景下 SOP 文档非常重------一个银行函证控制表的 SOP 几千字,加上 references 几万字。每个会话都全量塞进去,单次 LLM 调用就要几万 token。
三段式加载链路
启动期
SkillsManager 扫描 resources/skills/cash-and-cash-equivalents/
↓
抽取每个 SKILL.md 的 frontmatter(name + description)
↓
注入 <skills_system> 到 system prompt(仅元数据,几百字)
LLM 看到
"我看到有 'bank-confirmation-control-schedule' 这个 skill,描述是...,
现在用户问银行函证控制表,那我应该加载它"
运行期
LLM 调 load_enhanced_skill(skill_names=["bank-confirmation-control-schedule"])
↓
工具调 SkillsManager.loadSkillContent() 读全文
↓
全文包成 <loaded_skills><skill>...</skill></loaded_skills>
↓
注入到 systemPrompt(增量更新,老的 <loaded_skills> 被替换)
↓
写入 sessionLoadedSkills Map(跨 invoke 持久)
关键设计点
| 点 | 解读 |
|---|---|
| 元数据 vs 全文分离 | 元数据预注入是"目录",全文按需加载是"翻书"。LLM 先看目录决定翻哪本 |
| 覆盖更新而非追加 | load_enhanced_skill(skill_names=["A"]) 之后再调 ["B"],A 会被卸载,只剩 B。避免 LLM 不停堆叠 |
| 空数组卸载 | skill_names=[] 表示清空。让 LLM 主动管理上下文 |
| references 不预加载 | SKILL.md 全文加载了,但 references/ 仍按需用 bash_run 读。避免一次性全爆 |
实践经验:写一个好 Skill 的三条铁律
- frontmatter 的 description 是 LLM 决定加载的唯一依据------一定要写得具体("用于编制银行函证控制表,包括从明细表提取数据..."),别写"通用工具"这种废话
- SKILL.md 主体应该是"SOP 索引 + 步骤摘要",而不是"所有事实性知识的拷贝"。事实性知识放 references/,让 LLM 按步骤读
- 每个 references/*.md 应当独立可读------LLM 通常只读一份,不会通读全部
1.6 双存储模型 ------ "上下文真相"与"展示真相"分离
问题:为什么 risk-assessment 用一份存储,cash-and-cash 用两份?
risk-assessment 的做法(旧路):
session_messages 表
↓ 取出 → 转成纯文本 → 拼到 prompt
LLM
cash-and-cash 的做法(新路):
SDK Store(messages[],含 tool_use/tool_result)
↓ 自动恢复
Agent.messages
↓
LLM
完成后单向同步:
Agent.messages → AgentPersistedMessage → session_messages(前端展示)
为什么不能合二为一?
如果让 SDK 直接读 session_messages 作为上下文:
| 问题 | 后果 |
|---|---|
| 格式不同:SDK 用 ContentBlock(text/tool_use/tool_result),项目用 blocks(main_text/thinking/tool) | 转换层成本高、易错 |
tool_use 与 tool_result 必须配对 :Anthropic API 要求 tool_use_id 严格匹配 |
转换时只要一个 toolCallId 错位,整轮请求就被 reject |
| 生命周期不同:Agent 内部还可能压缩、合并消息 | 同步双向写会导致竞态 |
单向同步的核心原则
"Agent 上下文的真相 = SDK Store"
"前端展示的真相 = session_messages 表"
两者通过 MessagePersistenceAdapter 单向同步,
绝不让 session_messages 反向影响 SDK Store。
实践经验:什么时候双存储是合理的?
- ✅ 当 SDK 提供完整的消息生命周期管理(恢复、压缩、持久化)
- ✅ 当前端展示格式 ≠ LLM 消费格式
- ❌ 当你只是为了"省一次写"------那不值得。直接单存储,自己处理转换
1.7 EventBridge ------ 异构事件系统的"翻译机"
上下游事件模型对比
| SDK EventBus(三通道) | 项目 AgentStream(单通道 SSE) |
|---|---|
progress:text_chunk / think_chunk / tool:start / tool:end |
data 事件,chunk 字段含 type 区分 |
control:暂停/恢复/取消 |
无对应 |
monitor:监控用,不影响主流 |
无对应 |
SDK 事件 项目事件
text_chunk_start → text-start
text_chunk(delta) → text-delta
text_chunk_end → text-end
think_chunk* → thinking-*
tool:start → tool-call
tool:end → tool-result
done → complete
为什么需要翻译
不翻译会怎么样?
- 前端要重写:Cherry Studio 前端的 SSE 解析、消息块渲染都基于 AgentStream。如果直接发 SDK 事件,整套渲染管线得重写
- 新旧智能体不能共存:risk-assessment 已经用 AgentStream,新智能体如果发别的格式,前端要分支判断
- 职责模糊:把"事件翻译"放在前端意味着前端要懂 SDK,违反分层
设计原则
"统一对外接口":不同 Runtime 内部实现可以差异巨大(自写 ReAct vs SDK Agent vs 工作流引擎),但暴露给上层的事件流必须是同一种格式。
这就是 EventBridge 的全部价值------把内部异构性吞进去。
实践要点
| 要点 | 说明 |
|---|---|
| 事件映射要可逆 | 调试时能从前端事件反推 SDK 事件,便于排查 |
| 错误事件要兜底 | 任何 SDK 抛异常都翻译成 {type:'error'},再补一个 {type:'complete'},否则前端永远 loading |
| 不要丢事件 | EventBus 用 timeline + cursor 保证有序,订阅端必须 await 完所有迭代再 stop |
1.8 Hook 机制 ------ 工具调用前后的"哨兵"
Hook 是什么
{
preToolUse: (call) => {
// 工具执行前。可以:
// - 改写参数(call.args.cmd = ...)
// - 拒绝执行(return { allow: false })
// - 记录监控
},
postToolUse: (outcome, ctx) => {
// 工具执行后。可以:
// - 校验结果
// - 触发提醒(agent.remind())
// - 副作用(重置计数器)
}
}
为什么 Hook 是范式级的设计
传统做法是把这些逻辑写进工具内部。问题:
| 写在工具里 | 写在 Hook 里 |
|---|---|
| 工具变得臃肿,关注点混杂 | 工具保持原子,纯执行 |
| 不同工具的横切逻辑(命令改写、合规检查)要复制 | 一处定义,全工具生效 |
| 难以为工具添加监控 | Hook 天然是观察者 |
| 测试工具时要 mock 监控 | 工具与 Hook 解耦,独立测试 |
货币资金智能体的 Hook 全景
preToolUse
├─ bashCommandRewriter ← 改 bash_run 的 cmd(python -c → 写脚本)
└─ recordToolUse ← 给 SkillComplianceMonitor 喂数据
postToolUse
└─ onSkillLoaded ← 检测到 load_enhanced_skill 成功 → 触发 remind
+ 重置 33 轮窗口
实践经验:Hook 该写什么、不该写什么
| 该写 | 不该写 |
|---|---|
| 横切关注点(监控、日志、改写、重试) | 业务逻辑(应该在工具里) |
| 跨工具的共性逻辑 | 单工具特有的处理 |
| 防御性兜底(参数补全、格式归一) | LLM 应该自己想清楚的事 |
黄金法则:Hook 是"基础设施",工具是"业务"。基础设施不该知道业务细节,业务也不该重复造轮子。
第二部分 智能体范式的学习经验
下面这一部分是踩坑后才懂的事。它不是"应该怎么做"的清单,而是"做错了会怎样"的教训。
2.1 经验一:智能体不是"更聪明的程序",而是"会做决策的员工"
心智模型转换
刚接触智能体的开发者最容易犯的错是 "用写程序的方式写智能体":
- 用 if-else 限制 LLM 的下一步动作
- 在 prompt 里写"步骤一做 X,步骤二做 Y",把流程钉死
- 工具内部判断"如果 LLM 给的参数不对,自己补全"
这套做法表面上让智能体"更可靠",实际上:
- ❌ 牺牲了 LLM 的灵活性(它可能有更好的解法)
- ❌ 流程一变就要改代码(违背"自主"的初衷)
- ❌ 让代码越来越像传统业务系统
正确的心智模型是 "实习员工":
| 维度 | 程序员视角(错) | 雇主视角(对) |
|---|---|---|
| 给指令 | 写死流程 | 给目标 + 边界 |
| 给资源 | 暴露 API | 给工具 + SOP(Skill) |
| 出问题 | 调试代码 | 给反馈、改 SOP |
| 期待 | 100% 确定性 | 概率上正确,配合 harness 兜底 |
实践转化
写一个新智能体时,先回答三个问题:
- 它的角色是谁?(写进 system prompt 静态段)
- 它能用什么工具?(注册到 ToolRegistry)
- 它怎么知道做什么?(Skill 系统 / instructions)
而不是先问 "它的状态机有几个节点"。
2.2 经验二:上下文是稀缺资源,必须像 GC 一样管理
上下文的"四类敌人"
LLM 上下文窗口有限(即使是 1M token 的 Claude,实际有效注意力也会随长度衰减)。让 LLM 能稳定工作的第一原则是:保持上下文干净。
四类常见污染:
| 类型 | 例子 | 危害 |
|---|---|---|
| 巨型 tool_result | read_table 直接返回 10 万行数据 |
后续每轮都要重新读,token 爆炸 |
| 图片 / Base64 | 视觉调用直接把图片塞回主上下文 | 一张图轻松吃 5k token |
| 冗长思考链 | 模型 reasoning 不压缩 | 几轮后 thinking 占了 60% |
| 重复元数据 | 每轮都在 prompt 里重复"你是审计师..." | 静态段重复 N 次 |
货币资金智能体的应对
每一类都有专门的设计:
| 敌人 | 应对 |
|---|---|
| 巨型 tool_result | fs_write 写脚本,bash_run 执行------结果存盘,对话只回传文件路径与摘要 |
| 图片 | vision_subagent 隔离------图片只在子智能体上下文里,主智能体只看到子智能体的结论文本 |
| 冗长思考 | SDK ContextManager 自动压缩(compressToTokens: 60k)+ DeepSeek 的 stripFromHistory |
| 重复元数据 | 静态段不重复;动态段用 XML 标签,工具修改时 替换而非追加 |
实践经验:上下文预算思维
设计每个工具时,问自己:
"这个工具的输出会进入主上下文吗?如果会,最坏情况会有多大?"
如果答案是"可能很大",那么必须做以下之一:
- 把大输出落盘(fs_write),返回路径
- 用子智能体隔离(vision_subagent 模式)
- 在工具层做摘要(artifacts 引用而非内联)
2.3 经验三:让 LLM 自己做决策,但用 Hook 兜底
矛盾点
智能体范式要求 LLM 自主决策,但 LLM 不可靠:
- 它会忘记加载 Skill
- 它会重复犯同样的错
- 它会在死胡同里转圈
解法不是"剥夺它的决策权",而是"在它出错时悄悄拉一把"。
货币资金智能体的"Nudge 体系"
这套设计非常精巧,值得仿照:
LLM 调用 load_enhanced_skill 加载 SOP
↓
PostToolUse Hook 触发 agent.remind()
"skill 已加载,记得读 references/ 目录的具体步骤"
↓
LLM 接到 system-reminder(不是 user 消息!优先级高于 user)
↓
LLM 按提醒去读 references
------继续工作 33 轮------
SkillComplianceMonitor 检查滑动窗口
"最近 33 轮工具调用,没有任何一次读 references"
↓
触发 agent.remind()
"请按 SOP 走,事实性知识在 references 目录"
↓
LLM 收到,回到正轨
关键设计点
| 点 | 解读 |
|---|---|
| 用 remind() 而非 user 消息 | system-reminder 优先级最高,不会被 ContextManager 压掉 |
| 基于事实而非猜测 | 滑动窗口检查的是"调用记录",不是"看 prompt 里有没有提到 references"------前者可验证 |
| 熔断保护 | 33 轮窗口检查 + 88 轮强制提醒,两道闸门 |
| 不阻塞 LLM | remind 是异步的提示,LLM 仍然可以选择忽略(罕见但允许) |
实践经验:Nudge 设计的三原则
- 可观测:监控的指标必须是工具调用记录、文件读写等可验证事件,不是"prompt 里写了什么"
- 温和:用 remind 而非强制------LLM 可能有更好的判断
- 递进 :先即时提醒,再窗口检查,再强制熔断。给 LLM 自我纠错的机会
2.4 经验四:嵌套智能体是"复杂任务的杀手锏"
什么时候需要嵌套?
不是越多智能体越好。嵌套的代价是:
- 多一倍的 LLM 调用成本
- 调试复杂度增加(两个 loop 嵌套)
- 错误传播路径变长
但有些任务没有嵌套就做不了。判别标准:
| 特征 | 说明 |
|---|---|
| 大量重复处理 | 处理 50 张银行回函(vision_subagent Mode 3) |
| 不同的上下文需求 | 主任务关心结构化数据,子任务关心图片像素 |
| 需要独立的"工作记忆" | 子任务的探索期需要清空重启 |
| 结果是"小而结构化"的 | 子任务返回结构,不污染主上下文 |
vision_subagent 完美命中所有四条。
嵌套与并行的区别
嵌套(vision_subagent) SDK 的 delegateTask(并行)
主智能体 ─→ 调工具 ─→ 子 Loop 主 ─┬─→ 子 1
(阻塞) ├─→ 子 2 (同时跑)
返回结果 └─→ 子 3
聚合
货币资金选嵌套,是因为 vision Mode 3 需要主智能体一直挂着,等 vision 子智能体跑完一整个 CSV 再回来。如果改成并行,主智能体得自己写 join 逻辑,复杂度反而升高。
实践经验:嵌套智能体的三个必备设计
- 超时与心跳 :子智能体可能跑很久,必须有独立超时(
calcTimeoutMs)+ 心跳保活 - 可恢复的进度:子智能体崩了怎么办?用 progress.json 落盘,下次能续作
- 结构化返回 :子智能体不能返回"我做完了",要返回
{processed_codes, total_images, summary_csv_path}让主智能体能续作
2.5 经验五:状态机 + 双阶段循环 = 处理批量任务的范式
vision_subagent Mode 3 的设计值得专门拎出来讲。它解决的是 "用有限上下文处理大量同质任务" 这个普遍问题。
朴素做法的失败
循环 50 次:
load_image(i)
VLM 分析 → 提取数据 → 追加 CSV
跑到第 10 张图,对话历史里有 10 张图的 base64 + 10 次分析记录,上下文炸了。
Mode 3 的状态机解法
阶段 EXPLORE(采样 6 张图)
↓ VLM 学会了:表头是什么、关键信息在哪
↓ 调 conclude_and_compact 工具
├─ Hook 校验 CSV 真的写了
├─ 写 progress.json
└─ 清空 messages,只留 system prompt
↓
阶段 EXECUTE(每张图独立处理)
↓ load_image → 提取 → 追加 CSV
↓ 每 N 张再 conclude_and_compact 一次(清上下文)
↓
阶段 DONE(processed_codes == total_images)
设计精髓
| 设计 | 解决的问题 |
|---|---|
| EXPLORE / EXECUTE 两阶段 | 探索时需要全文样本对比;执行时只关心当前一张 |
| conclude_and_compact 工具 | 让 LLM 主动声明"我学会了",触发上下文重置 |
| PreToolUse Hook 校验 CSV mtime | 防止 LLM 谎称"我处理完了"------必须有真实输出 |
| progress.json 落盘 | 重置 messages 后,下一轮还能从磁盘恢复"已处理列表" |
| CSV 直接追加而非内存累积 | 数据持久化在文件里,不依赖上下文 |
这是一种"范式"而不是"技巧"
当任务是"对 N 个相似对象做同样处理 + 生成结构化输出"时,用"探索期建模 + 执行期单元处理 + 进度落盘"的三段式。
不仅 vision Mode 3 适用。下一个智能体如果要处理 100 张发票、500 条银行流水、200 个合同------都可以套这个范式。
2.6 经验六:失败是可观测的,不是 silent 的
反例:silent failure
裸 LLM 的常见失败:
- 模型一次 fetch failed,整个 loop 崩,前端永远 loading
- 工具执行报错,LLM 假装没看见继续编
- 上下文超长,模型默默截断,结果失真
这些失败 没有异常、没有日志、没有提醒------你不打开 DevTools 永远不知道。
货币资金智能体的"可观测性栈"
loggerService(每个模块带 context)
↓
SDK Monitor 通道(tool_executed / error / context_compression)
↓
ModelRetryHandler(吃掉暂态错误,但记日志)
↓
SkillComplianceMonitor(异常调用模式触发提醒)
↓
EventBridge 兜底 error → complete(保证前端不卡住)
每一层都对失败有响应:要么自愈,要么记录,要么向上抛。没有任何失败被吞。
实践经验:可观测性的三个层次
| 层次 | 例子 | 作用 |
|---|---|---|
| 日志 | logger.error('Agent loop error', {...}) |
事后排查 |
| 指标 | tool_executed 计数、context_compression 次数 | 趋势监控 |
| 反馈 | agent.remind() / 用户可见错误事件 | 实时纠偏 |
裸 LLM 是黑盒,加了 harness 才是白盒。
第三部分 Harness 工程的实践观点
"Harness" 这个词来自西方驯马术------给马套上的挽具。它的意思是:给一个本来不可控的东西套上一组装备,让它能稳定干活。
LLM 就是那匹野马。Harness 工程是给它套挽具的工程。
下面是从货币资金智能体提炼出的、可复用的 Harness 实践观点。
3.1 观点一:把 Harness 视为"产品的一部分",不是"调试工具"
错误认知
很多团队的开发顺序是:
- 先把核心功能做出来
- 上线后出问题,加日志、加重试、加监控
- Harness 是"补丁"
这种顺序的问题:harness 是事后的,与核心代码耦合差,加一个补丁多一个隐患。
正确认知
Harness 应该 从第一行代码就是架构的一部分。具体到货币资金智能体:
| Harness 组件 | 何时引入 |
|---|---|
| ModelRetryHandler | Runtime 初始化时就 attach |
| SkillComplianceMonitor | constructor 里就实例化 |
| EventBridge 错误兜底 | EventBridge 本身的一部分 |
| Hook 注册 | invoke 路径上必经环节 |
它们不是"事后加的层",而是 Runtime 的核心组成。少了任何一个,Runtime 就跑不了。
3.2 观点二:Harness 的设计层次 ------ "防、查、补、断"
四个层次,对应不同的问题类型:
| 层次 | 关键词 | 例子 |
|---|---|---|
| 防 | 不让错误发生 | bashCommandRewriter(提前改写 python -c) |
| 查 | 发现错误 | SkillComplianceMonitor(监控 references 读取) |
| 补 | 错误发生时自愈 | ModelRetryHandler(暂态错误重试) |
| 断 | 严重错误时熔断 | conclude_and_compact 三连失败终止 |
实践原则
能防就不查,能查就不补,能补就不断。
理由:每一层的成本都比上一层高。能在 Hook 里改写命令,就别让 bash_run 报错;能监控提醒,就别让任务跑废重做;能重试就别让用户看到错误;只有真的没救了才熔断。
货币资金智能体的"四层"分布
防:bashCommandRewriter、模板浅克隆、单向同步原则
查:SkillComplianceMonitor、tool_executed 计数器、PreToolUse Hook 校验
补:ModelRetryHandler、agent.remind() 提醒、ContextManager 压缩
断:vision Mode 3 三连失败熔断、超时控制、错误事件兜底
3.3 观点三:Harness 不是越多越好,要有"边际收益"判断
反例:过度 Harness
工具调用 → Hook 1 → Hook 2 → Hook 3 → ...
检查 A 检查 B 检查 C
每个 Hook 都"为了安全"加一个检查。后果:
- 性能下降(每次工具调用要走 N 个钩子)
- 调试困难(出错了不知道是哪层 Hook 的问题)
- 维护噩梦(10 个 Hook 互相影响)
判断 Harness 是否值得加的"三问法"
加任何一个 Harness 之前,先问:
- 这个错误真的会发生吗? (不是理论上会,而是实际数据里见过)
- 不加这个 Harness,错误的代价是什么? (用户重试一次 vs 数据损坏)
- 加了这个 Harness,会不会引入新的复杂度?
货币资金智能体的所有 Harness 都通过了这三问。例如 bashCommandRewriter:
- 实际数据里
python -c多行代码失败率高(docs/数据/bash_run_issues_grouped.json有记录) - 不加,LLM 会陷入"写代码 → 执行失败 → 重写 → 再失败"的死循环,浪费数十轮
- 加了只是一个改写函数,复杂度可控
3.4 观点四:Harness 必须可观测、可配置、可禁用
可观测
每个 Harness 组件都要回答:"它现在在做什么?做了多少次?"
logger.info('bash_run command rewritten by hook', {
agentId,
original: originalCommand.substring(0, 200),
rewritten: rewrittenCommand.substring(0, 200)
})
货币资金智能体的所有 Harness 都打 info 级日志。调试时能直接看到 Harness 在工作。
可配置
阈值不应该写死。SKILL_UPDATE_REMINDER_THRESHOLD = 88、CHECK_WINDOW = 33、compressToTokens = 60_000 都应该是可调的。生产环境的最优值与开发环境往往不同。
可禁用
Harness 偶尔会"过度反应",干扰正常工作。比如:
- ModelRetryHandler 重试一个永久错误
- SkillComplianceMonitor 在不需要 Skill 的简单任务上反复提醒
设计时要预留 关闭开关。例如通过环境变量、配置项、特殊 prompt 标记。
3.5 观点五:Harness 的实施顺序 ------ "可观测 → 验证 → 防御 → 优化"
不要一开始就上一堆 Harness。正确的顺序:
Phase 1:可观测先行
loggerService + Monitor 通道
"我先要知道 LLM 在做什么"
什么都不加,先收集 1-2 周的真实运行数据。看看:
- 哪些工具调用最频繁
- 哪些步骤失败率最高
- 上下文增长曲线如何
Phase 2:基于数据上验证
SkillComplianceMonitor + tool_executed 计数器
"针对高频问题做监控"
挑出 Phase 1 数据里的痛点,加监控(不改行为,只观察)。
Phase 3:再上防御
bashCommandRewriter + ContextManager 压缩
"把已验证的问题在源头堵掉"
Phase 2 数据证明这个问题真的影响 LLM 完成任务,且改写/重试能解决,再加防御。
Phase 4:最后做优化
并行化、缓存、模型分层
"在已经稳定运行的基础上提升效率"
货币资金智能体的优化路线图(见 优化设计.md)正是这个顺序:先 E5(可观测)→ 再 R + A2(验证)→ 再 W1 + A3(防御)→ 再 E2 + E4(并行优化)。
3.6 观点六:Harness 的"反 pattern"清单
写完所有正面观点,列一些典型反 pattern作为警示:
| 反 pattern | 为什么错 |
|---|---|
| 在工具内部写监控 | 工具应该只做事;监控写在 Hook 里 |
| 静态 prompt 里写"不要做 X" | LLM 大概率忽视;用 PreToolUse Hook 强制阻止 |
| 重试无限次 | 永久错误会让重试器空转;必须设上限(货币资金用 3 次) |
| 吞掉所有异常 | 隐藏问题,下次出事更难查;必须 logger.error |
| Harness 之间互相依赖 | 改一个炸一片;保持 Harness 之间独立 |
| 没有可禁用开关 | 排查问题时无法二分定位 |
| 用 user 消息做提醒 | LLM 会把它当成新指令;用 system-reminder(remind()) |
| 跨 session 共享可变状态 | 数据污染;用 Map 按 sessionId 隔离 |
3.7 观点七:Harness 是智能体范式的"成人礼"
最后一个观点,偏哲学但很重要。
三个阶段
观察一个团队/开发者写智能体,往往会经历三个阶段:
第一阶段:天真期
- "GPT/Claude 这么强,给个 prompt 就好了"
- 没有 Harness,全靠 prompt 工程
- 跑 10 次有 6 次成功,已经很满意
第二阶段:恐惧期
- 上线后发现:用户的怪异 case 让 LLM 各种翻车
- 开始疯狂加 if-else、加 fallback、加重试
- prompt 越写越长,代码越来越像传统业务系统
- 智能体的灵活性逐渐丧失,回到"程序"的形态
第三阶段:成熟期
- 接受 LLM 是不确定的、概率的、会犯错的
- 不再试图"消除"错误,而是"管理"错误
- 用 Harness 在 LLM 周围搭一个"安全网"
- LLM 在网内自由发挥,错误被网兜住
货币资金智能体处于第三阶段。它的代码量看起来不少,但你会发现:
- 静态 prompt 很短,没有"步骤一步骤二"的硬编码
- 工具描述是"做什么",不是"什么时候做"
- 流程没有写在代码里,写在 Skill 的 SOP 里
- Harness 在边缘默默工作,不干扰主流程
一句话总结
第一阶段相信 LLM,第二阶段不相信 LLM,第三阶段在不相信的前提下重新相信 LLM。
Harness 工程就是这种"重新相信"的物理基础。
第四部分 总结 ------ 把这套方法迁移到下一个智能体
如果你即将动手做"应收账款智能体""存货智能体"等下一个 SDK 原生智能体,下面是迁移清单。
4.1 必做项(缺一不可)
┌──── 注册层 ──────────────────────────────────┐
│ □ AGENT_TYPES 加新类型 │
│ □ capabilities/definitions/<type>.ts │
│ └ 注册 PluginBinding(含 skillPaths) │
│ □ AgentRegistry.register({type, runtime}) │
└──────────────────────────────────────────────┘
┌──── Runtime 五件套 ──────────────────────────┐
│ □ <Type>Runtime.ts (实现 AgentRuntime) │
│ □ ModelConfigResolver / 复用 cce 的 │
│ □ AppModelFactory / 复用 cce 的 │
│ □ EventBridgeAdapter / 复用 cce 的 │
│ □ MessagePersistenceAdapter / 复用 cce 的 │
└──────────────────────────────────────────────┘
┌──── Skill 资源 ──────────────────────────────┐
│ □ resources/skills/<type>/<skill-name>/ │
│ ├ SKILL.md(含 frontmatter) │
│ └ references/ │
└──────────────────────────────────────────────┘
┌──── 工具集 ──────────────────────────────────┐
│ □ bash_run + fs_write(直接复用 SDK) │
│ □ conclude(视需要) │
│ □ load_enhanced_skill(视需要) │
│ □ 业务专属工具 │
└──────────────────────────────────────────────┘
┌──── Harness ─────────────────────────────────┐
│ □ Logger(每模块带 context) │
│ □ ModelRetryHandler(暂态错误重试) │
│ □ ContextManager 配置(maxTokens等) │
│ □ EventBridge 错误兜底 │
└──────────────────────────────────────────────┘
4.2 视情况添加
┌──── 高级 Harness ────────────────────────────┐
│ □ SkillComplianceMonitor(如果 Skill 加载频繁)│
│ □ 业务专属 Hook(如 bashCommandRewriter) │
│ □ tool_executed 计数器(防 LLM 走偏) │
│ □ vision_subagent(如果涉及图片处理) │
│ □ 状态机循环(如果涉及批量处理) │
└──────────────────────────────────────────────┘
4.3 慎选项(一定不要复制)
┌──── cce 的特殊设计 ──────────────────────────┐
│ ✗ <skills_system> 清理(cce 特有的冲突,新 │
│ 智能体如果不冲突就别清) │
│ ✗ 88 轮 skill 更新提醒(业务相关,重新选阈值)│
│ ✗ vision Mode 3 状态机(仅适合大批量图片) │
└──────────────────────────────────────────────┘
4.4 心法
最后留三句话:
1. 先想清楚智能体的"角色",再写代码。 角色不清,工具就乱。
2. 上下文是预算,每个工具都要为它的输出大小负责。 别让一个工具污染整个对话。
3. Harness 不是炫技,是兑现"智能体范式"承诺的工程基础。 没有 Harness 的智能体只是个 demo。
附:推荐阅读路径
| 顺序 | 文档 | 重点 |
|---|---|---|
| 1 | 学习文档.md |
全景理解 |
| 2 | 本文档 | 范式与 Harness 深度 |
| 3 | 架构设计.md |
编排层与 Runtime 协作 |
| 4 | 系统提示词组装方案.md |
动态段设计细节 |
| 5 | 技能架构设计.md |
Skill 注册全链路 |
| 6 | vision_subagent架构设计.md |
嵌套智能体范式 |
| 7 | 工具长时程运行设计.md |
心跳与超时 |
| 8 | 优化设计.md |
当前架构问题诊断 |
| 9 | SDK 内部 packages/kode-agent-sdk/docs/ |
底层机制 |
读完这条路径,应该能回答:
- 为什么货币资金智能体选择基于 SDK 而不是自写?
- 如果要做应收账款智能体,第一步该改哪几个文件?
- 当 LLM 在第 50 轮还没读 references 时,到底是哪个组件触发的提醒?
- vision Mode 3 处理到一半进程崩了,重启后能续上吗?为什么?
如果都能答上,恭喜你已经从"看懂代码"升级到"理解范式"了。
补充专题:Agent 落地讨论与路线图
Agent 落地与 Harness 工程化:从"自主性"到"可验证执行"
基于近期讨论整理,更新于 2026-07。本文讨论的不是某个 Agent 框架怎么用,而是:如何把概率性的模型能力,装进一个可观测、可验证、可恢复、可治理的生产系统。
先给结论
- Agent 和 Workflow 不是非此即彼。 更有用的划分是:哪些决策交给确定性代码,哪些决策交给模型。生产系统通常是"确定性骨架 + 局部模型自治",而不是纯工作流或无限循环的纯 Agent。
- Agent 的产品能力不等于模型能力。 可近似写成:
Agent 系统能力 = 模型 × Harness × 工具/环境 × 上下文 × 验证器 × 运行预算。任何一项接近零,整体成功率都会坍塌。 - Harness 不是一层 Prompt 包装。 它是模型之外的执行控制面:状态管理、工具路由、权限、预算、重试、检查点、验收、追踪、回放和人工接管都属于 Harness。
- 自主性应放在"路径选择",确定性应放在"边界与验收"。 不必规定 Agent 每一步怎么走,但必须规定它能做什么、花多少、什么叫完成、证据是什么、失败后回到哪里。
- 评测不能只看最终回答。 至少同时看最终环境状态、过程轨迹、重复运行的稳定性、成本/时延和安全性。Agent 声称完成不是完成,环境中的可验证状态才是完成。
- "只完成一半却宣布完成"不是简单的幻觉问题。 它通常是任务契约、状态表示、终止条件和验收机制共同缺失的结果;换更强模型能缓解,却不能替代工程闭环。
一、Agent 到底是什么:不要争名词,要标注决策权
业内把很多东西都称为 Agent:固定工作流外包一层 LLM、带工具调用的 ReAct 循环、Skills 渐进披露、能长期修改环境的 Coding Agent,甚至多 Agent 协作系统。这些叫法都能成立,但抽象层次不同。
一个更便于工程沟通的连续谱是:
| 形态 | 谁控制下一步 | 优点 | 主要风险 | 适合场景 |
|---|---|---|---|---|
| 确定性 Workflow | 代码/规则 | 稳定、易审计、易定位 | 长尾规则膨胀 | SOP 清楚、错误代价高 |
| Agentic Workflow | 代码控制主干,模型决定局部分支 | 自主性和可控性平衡 | 边界设计复杂 | 大多数企业生产场景 |
| 单 Agent Loop | 模型动态规划和选工具 | 灵活、覆盖开放任务 | 漂移、误终止、成本波动 | 研究、分析、编码、低风险开放任务 |
| 多 Agent 系统 | 多个模型角色协作/委派 | 上下文隔离、专业化、并行 | 协调税、错误传播、评测困难 | 可清晰分域或可并行的复杂任务 |
OpenAI 的实用定义强调:模型要能管理工作流执行、动态选择工具、识别完成并在失败时纠正或移交;Anthropic 则明确区分预定义代码路径的 workflow 与模型动态控制过程的 agent。两者真正一致的地方是:判断一个系统是否"Agentic",看模型是否拥有执行路径的决策权,而不是看它有没有聊天界面或 tool call。
因此,与其问"这到底算不算 Agent",不如为每个环节写出一张决策权表:
- 路由由规则还是模型决定?
- 参数由模板、模型还是二者共同生成?
- 终止由模型声明,还是由外部验收器判定?
- 失败由固定策略重试,还是由模型重新规划?
- 写操作是否需要策略引擎或人工批准?
这张表比名词更能揭示系统的稳定性、成本和责任边界。
二、Harness:Agent 的"执行操作系统"
Prompt 告诉模型"应该怎么想",Harness 决定系统"实际上允许怎样运行"。可以把 Harness 分成六层:
1. 任务契约层
把模糊 goal 编译成机器可检查的任务契约:目标、范围、约束、交付物、禁止事项、验收标准和停止条件。对长任务,维护显式的任务账本,而不是只依赖对话历史:
goal: 完成客户流失分析并生成可复核报告
deliverables:
- analysis.md
- query.sql
- result_snapshot.csv
acceptance:
- SQL 可执行且无截断
- 报告中的数值均能映射到结果行
- 时间范围、口径和过滤条件已披露
status: in_progress
open_items: []
evidence: []
任务拆解的价值不是让步骤全部写死,而是建立进度状态和可局部验收的边界 。模型可以动态修改计划,但不能无证据地把任务从 in_progress 改为 done。
2. 状态与上下文层
上下文窗口不是可靠数据库。长任务需要把状态外置为可恢复的工件:任务清单、决策日志、已验证事实、失败记录、产物索引、版本控制和检查点。
Anthropic 的长时 Agent 实践发现,仅靠上下文压缩不够:Agent 会一次做太多,留下半成品;后续会话也可能看到已有进展便误判全部完成。其有效做法是初始化环境、按增量推进,并通过进度文件和 Git 历史向下一会话交接。这证明了一个重要范式:Context engineering 的重点不是塞入更多文字,而是让模型在正确时机读取正确状态,并把关键状态沉淀到上下文之外。
3. 工具与环境层
工具不是普通 API 的机械暴露。Agent-facing API 应满足:
- 名称和描述能区分相邻能力,参数有严格 schema;
- 返回结构同时包含结果、状态、分页信息、错误类型和可引用证据;
- 写操作支持幂等键、dry-run、事务或补偿操作;
- 工具集按任务动态披露,避免一次暴露数百个相似工具;
- 权限沿用用户身份,遵循最小权限,不给 Agent 一个万能服务账号;
- 不把微服务拓扑直接当作 Agent 的工具拓扑。
最后一点尤其关键。微服务按组织、数据所有权和部署边界拆分;Agent 工具应按用户意图和业务能力设计。一个业务操作若跨十个微服务,应优先由稳定的领域工具封装,而不是让模型每次临场拼十次底层调用。
4. 控制与恢复层
控制面应包括最大轮数、token/时间/金额预算、超时、取消、重试分类、熔断、并发控制、检查点和恢复。
重试也不能一概而论:网络超时可以原参数重试;参数错误应把结构化错误返回模型修正;权限错误不该重试;副作用不明时必须先查幂等状态。复杂流程最好使用 durable execution:每个有副作用的步骤持久化,进程崩溃后从安全检查点恢复,而不是整条链重跑。
5. 验证与终止层
Agent 的 final answer 只能表示"模型想结束",不能作为"任务已完成"的唯一条件。正确的结束协议是:
- 模型提交完成声明和证据清单;
- 外部验证器检查环境状态与验收断言;
- 验证通过才提交
success; - 验证失败则返回缺口,继续修复;
- 超预算、连续失败或高风险则进入
blocked/escalated,不能伪装成功。
验证器应优先采用确定性信号:单元测试、数据库断言、schema 校验、静态分析、引用覆盖率、文件存在性、业务状态机。LLM-as-judge 适合评价语义质量、风格或开放性结果,但应有清晰 rubric,并定期用人工样本校准。
6. 可观测与治理层
一次 Agent run 至少需要记录:模型与 Prompt/Skill 版本、输入、计划版本、每次工具调用及耗时、结果摘要、状态变更、重试、预算、验证结果、最终环境状态和人工介入。
追踪的目标不是展示一条漂亮的"思维链",而是回答:
- 是路由错、工具错、数据错、权限错、模型错,还是验收器错?
- 哪个版本开始退化?
- 同类失败占多少业务量?
- 修复应该落在 Prompt、工具、服务、数据语义层还是模型路由?
应记录可调试的 action/observation、状态和证据,避免依赖或暴露模型私有推理文本。生产 trace 经过脱敏后进入失败聚类,再回流为回归集,这才形成可持续优化闭环。
三、回答讨论中的三个核心问题
A. "怎么知道 Agent 变好了?"
"感觉更聪明"不是指标。应先定义业务效用,再拆成五类指标:
| 维度 | 示例指标 |
|---|---|
| 结果正确性 | task success、业务状态断言、事实/引用正确率 |
| 完整性与稳定性 | acceptance coverage、同任务多次成功率、pass@k、pass^k |
| 过程质量 | 工具选择准确率、无效调用率、重试恢复率、步骤/计划偏离 |
| 效率 | p50/p95 时延、token、工具调用数、单次成本、每成功任务成本 |
| 安全与体验 | 越权率、高风险误操作率、人工接管率、用户纠正率 |
其中 pass@k 回答"尝试 k 次至少成功一次的概率",偏能力上界;pass^k 回答"连续 k 次都成功的概率",更接近生产可靠性。对于每天大量重复执行的业务,后者往往更重要。
评测体系应有三层:
- 组件评测:路由、SQL 生成、检索、工具参数、摘要等单点能力;
- 端到端离线评测:在隔离环境运行完整 Agent,以最终环境状态评分;
- 线上评测:影子流量、canary、A/B、业务 KPI、人工反馈与事故指标。
数据集也要分层:核心能力集、历史失败回归集、长尾/对抗集、最新生产抽样集。每个 case 应多跑几次并报告置信区间,因为 Agent 输出具有随机性。
需要警惕 benchmark hacking。研究已经发现,一些 Agent benchmark 因任务环境或奖励设计缺陷,可能严重高估或低估能力。基准的 grader 本身也必须测试:是否存在"空回答也通过"、绕过预期路径、污染数据、测试覆盖不足等漏洞。Benchmark 不是一套题,而是一套需要被验证的测量仪器。
另外,比较模型时必须固定 Harness、工具、预算和评分;评估某个系统的最大能力时,则应给它合理优化过的 Harness。Harness 和推理预算会显著改变结果,因此任何 Agent 分数都应写全:model + harness version + tools + budget + dataset + grader。
B. "开放 Agent 怎么稳定,又不写死 Workflow?"
答案是约束边界,不约束路径。保留自主规划,但把系统设计成"自由度预算":
- 读操作可较开放,写操作按风险逐级收紧;
- 允许模型选择工具,但只披露当前域的候选集合;
- 允许动态改计划,但任务契约和验收标准不可自行降低;
- 允许自我修复,但设置最大重试、成本和时间;
- 允许提交结果,但必须带 provenance,由外部 verifier 决定是否完成;
- 可逆、低风险动作自动执行;不可逆、高金额、跨边界动作需批准。
可用一个简单决策原则决定"这一步该写死还是交给模型":
自治价值 = 长尾覆盖收益 - 失败损失 - 验证成本 - 时延/费用 - 合规成本
规则稳定、输入结构化、失败代价高的环节,交给代码;需要语义理解、开放搜索、例外处理的环节,交给模型;容易验证但难以生成的环节,尤其适合 Agent------例如写代码后跑测试。难生成又难验证的高风险任务,不适合直接全自动。
C. "几百个微服务里怎么找该改的项目?"
这不只是 service mesh 或 tool trace 问题,而是需要把业务意图、Agent 动作、分布式调用和代码所有权连成一条证据链:
user_goal → agent_run → plan_step → tool_call → domain_service → trace/span → error → repo/version → owner
落地要点:
- Agent run ID 注入所有下游调用,贯通分布式 tracing;
- 工具注册表记录 tool → API/服务 → repo → owner → schema/version;
- 错误分类区分模型选错工具、参数错、服务失败、数据质量和权限问题;
- trace 中保留 Prompt/Tool schema/模型版本,支持逐次回放;
- 以失败聚类和业务影响排序,不靠人工浏览海量轨迹;
- 用 contract test 检查工具层,避免下游服务变更悄悄破坏 Agent。
如果"一个操作需要模型直接挑选并组合大量微服务",根因通常不是 Agent 数量不够,而是缺少稳定的领域能力层。先做工具合并、命名去重、分域检索和候选集缩小;只有当上下文、权限、所有权或评测边界确实需要隔离时,再拆 Agent。
四、案例:开放数据查询 Agent 如何查全、有据、可恢复
原讨论把"只查一半"归因于 SQL 或工具错误,这只覆盖了一部分。至少要区分五类失败:
- 意图不完整:时间范围、指标口径、粒度或过滤条件不明确;
- 语义选错:找错表、join 错、把快照当流水、混用指标定义;
- 执行不完整:分页、行数上限、超时、采样、截断或部分分区失败;
- 综合不忠实:结果是对的,但回答补写了数据中不存在的结论;
- 错误终止:只完成子查询,却把局部结果当作最终结果。
推荐的执行协议不是"生成 SQL → 执行 → LLM 看一眼",而是:
- 问题规格化:输出指标、维度、时间、范围、口径和待澄清项;
- 语义检索:从数据目录、血缘、指标层、历史查询和 owner 注释中找候选;
- 生成查询计划:列出需要覆盖的子问题及 completeness 条件;
- 静态验证:解析 AST,做只读限制、表/列权限、危险语句、代价预估;
- 沙箱执行 :设置资源上限,返回
row_count / total_count / has_more / truncated / query_id / snapshot_time; - 结果验证:检查空值、重复、基数、时间覆盖、聚合守恒和交叉查询一致性;
- 证据化综合:每个关键数字绑定 query ID、结果列/行或结果快照;明确假设和未覆盖项;
- 完成门禁:只有所有子问题和 coverage assertion 通过,才允许回答"已完成";
- 持久化与恢复:保存计划、查询、快照和检查点,失败后从最近安全步骤继续。
OpenAI 内部数据 Agent 的公开实践与此相近:它用组织知识、代码、元数据和历史使用信息丰富数据语义;评测时为自然语言问题配人工 golden SQL,同时比较生成 SQL 和实际结果,而非只做字符串匹配;答案链接到底层结果,并继承用户原有权限。更值得注意的是,他们后来收紧和合并了重叠工具------工具越多不等于能力越强。
"SQL 语法解析器"只能证明语法合法,不能证明业务语义正确;"让另一个 LLM 复核"也不是独立真值。强验证应尽可能落到可执行断言和数据不变量上,LLM 负责补充语义判断。
五、AI Coding:如何发现 TODO、半成品和虚假完成
Coding Agent 是数据查询问题的同构版本:生成代码很开放,但成功通常较容易验证。不能只让模型自评,应使用分层完成门:
- 需求 → acceptance tests / 行为断言;
- 代码 → 编译、类型检查、lint、单测、集成测试;
- 变更 → diff scope、受影响模块和依赖分析;
- 完整性 → 扫描 TODO、placeholder、空实现、跳过测试和 mock 泄漏;
- 运行态 → 在临时环境启动并执行关键路径;
- 交付 → 要求变更摘要、测试证据、已知限制和未完成项;
- 高风险变更 → code review、安全扫描或人工批准。
关键不是禁止 TODO,而是禁止"未声明的 TODO 与成功状态并存"。真正无法完成时,合法终态应是 blocked 或 partial,并附缺口、原因和恢复点。
对于跨上下文长任务,建议采用:初始化任务账本 → 每次只完成一个可验收增量 → 测试 → 提交/检查点 → 更新进度工件 → 下一会话读取。Anthropic 公开实验中出现的"后续 Agent 看到已有进展就宣布结束",与讨论中的担心几乎完全一致,也说明这不是旧模型才会有的问题,而是长时自治的结构性风险。
六、是否应该拆成多 Agent
不要按微服务数量拆 Agent,也不要把"多 Agent"当作稳定性的默认答案。每增加一个 Agent,都会增加委派错误、上下文丢失、重复调用、延迟、成本和评测组合数。
更合理的拆分信号是:
- 工具相似度太高,单 Agent 持续选错;
- 不同子域需要独立权限或安全边界;
- 上下文互相污染,需要明确隔离;
- 子任务确实可以并行,且有清晰输入输出契约;
- 不同任务需要显著不同模型、提示、预算或验收器;
- 团队所有权本身清晰分域。
拆分顺序建议是:先优化一个 Agent 的工具设计和动态检索,再考虑 manager + specialists;只有在去中心化 handoff 确有价值时才使用对等多 Agent。多 Agent 的收益来自上下文与责任边界,不来自角色名更多。
七、成本、时延、质量与合规如何制衡
"解决方案就是加钱"只在原型阶段部分成立。生产优化应遵循:先用强模型建立质量上界和评测基线,再在可测量的节点降配。 不要一开始就处处用小模型,否则无法区分是架构失败还是模型能力不足。
常见策略:
- 规则处理确定性任务,小模型做分类/检索重排,强模型做复杂规划与失败恢复;
- 根据任务难度、风险和验证反馈动态升级模型,而非固定路由;
- 缓存稳定检索与工具结果,但携带数据版本和过期策略;
- 合并相邻工具调用,支持批量读和并行读;
- 把无关历史压缩成结构化状态,不盲目保留全部对话;
- 以"每个成功任务的总成本"优化,而不是只看单次 token 价格;
- 高风险动作增加验证和人工介入,低风险可逆动作追求自动化率。
安全方面,Prompt injection 不能只靠另一个分类模型拦截。Agent 同时接触不可信内容和高权限工具时,应采用 source-sink 思维:限制不可信信息可以影响的动作,敏感数据外发或不可逆操作必须有确定性策略、沙箱、最小权限与用户确认。
八、当前主要研究思路
截至 2026 年中,值得持续跟踪的研究和工程主线包括:
- 长时任务与时间跨度:能力不再只以单题准确率衡量,而是以某个可靠度下能完成多长的人类工时任务来衡量;长任务中的错误累积、恢复和跨上下文连续性仍是核心难题。
- Harness / elicitation 科学:同一模型因工具、上下文压缩、重试、预算和脚手架不同可呈现显著不同能力。模型与 Harness 的贡献如何解耦,是评测中的开放问题。
- 过程监督与结果验证:何时只看最终环境状态,何时检查轨迹;如何防止 grader 漏洞,又不惩罚 Agent 找到更优的新路径。
- 自适应规划与再规划:plan-and-execute、ReAct、反思/验证循环、搜索和 test-time compute 怎样按任务难度动态分配,而不陷入无效循环。
- 记忆与上下文工程:工作记忆、长期记忆、情景记忆如何写入、检索、遗忘和处理冲突;怎样避免记忆污染。
- 工具发现与委派:大规模工具下的语义检索、分层路由、动态披露;模型是否知道何时自己做、何时委派,以及如何评价路由质量。
- Agent 安全与权限:间接 Prompt injection、数据外泄、身份委托、最小权限、可撤销执行、审计和责任归属。
- 真实世界评测:从 GAIA、SWE-bench、WebArena、τ-bench 等任务集走向组织私有分布、动态环境、长期回归与经济指标。
九、大家共同面对、尚未彻底解决的制衡点
自主性 vs. 可预测性
自治越强,越能覆盖未预见路径,也越难穷举测试。当前最佳实践不是消灭不确定性,而是把它限制在可验证、可回滚的区域。
长上下文 vs. 正确信息密度
上下文更长不等于记忆更可靠。全量历史会增加噪声、冲突与"上下文焦虑";压缩又可能丢失关键承诺。结构化外部状态和按需检索比无限堆上下文更稳。
最终结果评测 vs. 过程评测
只看结果允许创新路径,但可能掩盖危险过程;强约束过程便于审计,却可能把 Agent 退化成 workflow。实践中应对结果做强断言,对高风险过程设策略,对普通路径保持弹性。
通用 Agent vs. 垂直 Agent
通用 Agent 复用好、覆盖广,却更难建立完备语义层和验收器;垂直 Agent 更容易可靠,但维护多个系统有成本。常见落点是共用运行时与 Harness,按领域配置工具、权限、知识和 eval suite。
单 Agent vs. 多 Agent
多 Agent 能隔离上下文和权限,却引入协调税。没有清晰契约、独立验收或真实并行性时,拆分通常只是把一个难调试循环变成多个难调试循环。
强模型 vs. 系统工程
强模型能减少大量边缘错误,但无法替代权限、事务、可恢复性与业务验收。反过来,Harness 也无法把能力不足的模型无限"抬升"。二者是乘法关系,不是替代关系。
十、一套可执行的落地路线
阶段 0:先判断是否需要 Agent
若规则清晰、路径稳定、失败昂贵,先做 Workflow;若核心价值来自开放语义判断、长尾和动态工具选择,再引入模型自治。
阶段 1:建立能力上界
选代表性任务和最强可用模型,构建最小单 Agent;定义任务契约和最终状态 grader。此时先证明"做得到",不要急着压成本。
阶段 2:构建 Harness 闭环
加入结构化状态、工具 schema、预算、trace、检查点、验收器、失败终态和回放。把线上失败持续转成回归 case。
阶段 3:影子运行与受控发布
只读 → 建议模式 → 人工批准写入 → 低风险自动写入,逐级放权。按风险而非按功能一刀切。
阶段 4:优化成本和复杂度
根据 trace 找真实瓶颈:模型路由、工具重叠、上下文噪声、服务延迟或验证开销。逐节点替换小模型、缓存、批处理或确定性实现,并用 eval 防回归。
阶段 5:再决定是否多 Agent
只有单 Agent 的工具混淆、上下文、权限或并行瓶颈被数据证实时再拆分;每个子 Agent 必须有明确契约、owner、预算和独立 eval。
结语
Agent 工程的主问题已经从"模型会不会调用工具"转向"系统如何可靠地承接一个概率性执行者"。真正成熟的 Agent 产品,不是让模型永不犯错,而是做到:
- 错误容易被发现,而不是被流畅语言掩盖;
- 失败停在安全状态,而不是留下未知副作用;
- 过程能够回放和归因,而不是靠拍脑袋调 Prompt;
- 能力提升能被评测证明,成本下降不以暗中降质为代价;
- 人只介入高风险和真正困难的部分,而不是给每一步兜底。
一句话概括:Workflow 规定路径,Agent 探索路径,Harness 管理探索,Verifier 决定是否抵达。
参考资料
- OpenAI:A practical guide to building agents
- OpenAI:Inside OpenAI's in-house data agent
- OpenAI:A shared playbook for trustworthy third-party evaluations
- OpenAI:Designing AI agents to resist prompt injection
- Anthropic:Building effective agents
- Anthropic:Effective harnesses for long-running agents
- Anthropic:Demystifying evals for AI agents
- LangGraph:Durable execution 与 Human-in-the-loop 文档
- Zhu et al.:Establishing Best Practices for Building Rigorous Agentic Benchmarks
- METR et al.:Measuring AI Ability to Complete Long Tasks