别把 Agent 的抱怨当成拆门指令

题记:自我改进的第一课,不是学会修 Bug,而是分清什么根本不该由自己修。
1. 抱怨可能是真的,修法仍然可以是错的⌗
设想一个纯粹虚构的场景:Agent 要调用一个会改变外部状态的 API,系统要求确认。它如实报告:“这一步需要等待,任务没能继续。”
现象完全真实。接下来应该修什么?
- 确认入口太难找,可以改交互。
- 等待状态没有保存,可以改续跑。
- 操作本来就需要确认,等待本身不一定是故障。
- 直接取消确认,确实最快,但那已经是在修改授权规则。
如果自我改进系统采用一条过于顺滑的链路:
Agent 遇到阻力 → 提交问题 → 自动修复 → 阻力消失
它很容易把最后一种当成成功。测试可以全绿,任务可以更快,系统也可以更不符合使用者的要求。
局部执行者知道哪里难受,却不天然拥有决定哪些阻力应该消失的权利。
本次设计调整的核心,是把 Agent 的抱怨从“修复工单”降回“现场证据”。不是不相信它,而是不让观察结果自动携带变更授权。
2. 先把采集接口做轻,把结论拿走⌗
report_friction 的普通上报只要求一个字段:symptom,即一句中立的现象描述。工具和组件名称可以补充;线程、回合及相关执行上下文由框架采集。
接口不再接收 fix、suggestion、workaround 或 kind 等字段。failure-reports.test.ts 对此有明确断言:只有现象也能上报,夹带这些额外字段则解析失败。
这不是指望一个 schema 就能读懂所有句子的语义。Agent 依然可能把建议塞进现象文本。它做的是一个更有限、也更实际的改变:不要在接口结构上邀请执行者同时扮演诊断者和方案批准者。
| 信息 | 应由谁提供 | 为什么 |
|---|---|---|
| 发生了什么、哪里费劲 | 当前执行的 Agent | 它离现场最近 |
| 发生在哪个回合、用了哪些工具 | 框架采集 | 不必依赖模型复述 |
| 多次现象是否同源 | 后续分诊 | 单个回合看不到全貌 |
| 修复是否改变既定约束 | 分诊与修复阶段检查 | 任务便利性不是唯一目标 |
| 是否允许放宽约束 | 使用者 | 规则的制定权没有转交给修复器 |
采集门槛低,行动门槛另算。让 Agent 随口报告小摩擦,不等于每句话都值得开一个分支。
3. 中间缺的不是一个分类器,是一个新的工作对象⌗
调整后的普通路径是:
抱怨事件 ──┐
自动信号 ──┼─→ 分诊:结合既有规则,解释共同根因
用户反馈 ──┘ │
▼
synthesis 信号
│
┌───────────┼───────────┐
▼ ▼ ▼
自动修复 不修/关联 交由用户决定
这里的关键是 synthesis:根因综合不是一段临时思考,而是单独入库、能够接受处置的信号。
ImproveService.synthesize 接收根因、成员信号、引用事件、核对过的规则,以及是否放宽约束的声明。它创建新信号,再把原来的成员关联过去。原始抱怨没有被覆盖成诊断结论,证据和解释仍然可以分开追溯。
与之配套,普通抱怨本身只能被关联为已知问题,或者说明理由后不修。直接对它选择自动修复,或通过公开工具启动修复,都会被拒绝。即使只有一条抱怨,也要先完成根因综合。
这一步的意义并不是“至少凑够几条才值得修”,而是强制发生一次问题重述:
系统应该满足什么需求,而不是 Agent 当时想移除什么障碍?
另一个容易忽略的细节是:计数聚合不等于根因聚类。
上报信号可以按工具或组件聚合次数,但 complaintEvents 会取回相应的可见事件,分诊不是只看最后一条摘要。同一工具的多次失败未必同因,不同工具的抱怨也可能指向同一个需求。根因不能从一个计数器里直接长出来。
4. “我检查过规则了”不能成为万能通行证⌗
综合后的根因仍然可能提出错误修法。所以这次改动不只增加一个分诊步骤,还增加了约束变更声明:
relaxesConstraint: boolean
它询问的是:这个修复是否放宽了使用者刻意设置的限制,或者扩大了 Agent 能做的事情?
在 constraints.ts 中,没有声明就不能继续相应的修复处置;声明为放宽约束而没有用户批准,也不能继续。这个检查覆盖代码修复入口,也覆盖知识修复和模型行为校准。
这是必要的:改变行为未必需要改 TypeScript。换一段提示词,也可能让系统开始做原先不允许的事。
实现还区分了真正的用户选择与自动应用的默认选项。批准查询明确排除 decided_via = 'auto',也不把“不修”或“继续讨论”当成放行。
不过,声明本身依然由模型作出。于是还有第二道、性质不同的检查:
| 检查位置 | 看什么 | 作用 |
|---|---|---|
| 分诊与修复入口 | 是否声明放宽约束、是否获准 | 拦住已识别出的权限变更 |
| 修复进行中 | 修复器是否发现方案需要放宽约束 | 将任务交回用户,而非继续换模型试做 |
| 提交落地前 | 分支是否触及登记的约束门代码路径 | 不只相信修复器的自我描述 |
CONSTRAINT_GATE_PATHS 登记了工具策略、批准执行、写入审查、预演、可见性等关键实现路径。未经相应批准的分支触及这些文件,会被暂缓落地;测试文件在部分目录规则中被排除。
代价很直白:这些文件中的无害重构也可能需要审批。文件路径检查不懂语义,它用一定的误拦换取可解释的拦截点。
它也不是完整证明。没有命中登记路径,不代表改动一定不会间接改变权限。语义声明与路径检查互相补位,但两者相加也不等于形式化验证。
5. 综合证据,不等于集中所有数据⌗
跨事件分析有一个诱惑:既然看得越多越容易找规律,就把所有抱怨交给同一个分诊 Agent。
这会让改进系统为了维护隐私边界,先跨过隐私边界。相当有程序员幽默感,但不该真的这么做。
这次实现保留了作用域:
- 抱怨事件查询使用当前查看者的可见性条件。
- 综合服务拒绝把不同隔离范围或类别的成员合成一个根因。
- 受限范围的抱怨由各自范围内的综合任务处理。
- 调度预检负责登记任务,再由运行时在对应范围内启动。
这带来一个明确取舍:某些跨范围的共同模式不会被一个全局模型直接看见。系统选择保留数据边界,而不是把“更聪明的诊断”当成扩大读取权限的理由。
还有一个速度上的例外:明确处于“等修复”状态的线程,仍然会立即进入修复路径。 普通抱怨可以等待综合;真正被阻塞的任务不必陪着排队。
因此,不能把这个设计描述成“所有抱怨都必须先经过每日综合”。它拆开的是常态采集与常态行动,同时保留阻塞任务的加急路径。加急改变调度优先级,不应被理解成批准取消约束;修复阶段的交回机制和落地检查仍然重要。
6. 测试证明了哪些事,又没证明哪些事⌗
这类改动最值得检查的,不是补丁行数,而是哪些原本顺手的调用现在会失败。
下面是落地测试中的断言范围,不是本次写作重新执行测试得到的运行报告:
| 测试文件 | 关键规格 |
|---|---|
failure-reports.test.ts | 现象可以单独上报;额外方案字段被拒绝;事件保留回合归属 |
triage.test.ts | 普通抱怨不能直接接受修复处置;分诊材料包含各条事件;综合后成员关联到根因 |
triage.test.ts | 缺少约束声明会被拒绝;放宽约束而未经批准不能自动修复 |
autofix.test.ts | 修复器发现约束变更时交回用户;改到约束门的分支不会直接进入落地 |
immediate.test.ts | 受限范围内的普通抱怨不逐条启动修复;对应综合任务在本范围启动;等修复路径仍然立即启动 |
这些测试能证明程序入口和状态转换被约束了,不能证明模型每次都能找到真正的根因。
同样,rulesChecked 是一份需要填写的核对记录,不是规则推理正确性的机器证明。服务会检查必填、成员范围与引用事件的可见性,但“检索是否充分”“规则是否读对”“这些事件是否真的同因”,仍然需要判断。
这不是设计失败,而是应该诚实标出的能力边界:确定性的程序负责让绕过步骤变难,模型负责解释问题,用户保留改变原则的决定权。
7. 真正应该优化的,不是阻力最小⌗
相比“发现问题就开自动修复”,这套设计多了一次综合、一些状态转换,以及偶尔让人觉得多余的审批。
它换来的不是修复速度必然更快,也不是故障率已经下降——代码和这些测试没有提供这样的生产统计。
它换来的是更清晰的权力划分:
Agent 可以广泛报告不便,分诊可以提出解释,修复器可以改善实现,但它们不能仅凭“不方便”就认定原来的规则应该消失。
自我改进系统最危险的成功,不是修不好,而是把不该修掉的东西修得干干净净。
门确实会挡路。那通常也是装门的原因。