综合案例

全程拆解B|一次仓库修复的每轮输入、输出、工具和恢复

综合案例约 4.7 千字1 组分步图解

下面是一段作者设计的协议级教学轨迹,不是从商业LLM运行中导出的真实会话。工具结构是简化的Harness自定义格式,不要求与任何厂商JSON字段完全相同。它用于回答:“Agent多次调用模型时,每一次到底给了什么、拿回什么、程序如何利用?”[TOOLS][CODEX][COMPACT]

任务:修复exclude功能的路径处理。约束:模式按项目根目录的相对路径匹配;不改包含规则;不提交、不部署。把最后两项保留下来,是为了防止“修好代码”被扩展成未经授权的交付动作。

本章目录

沿着对象与状态理解本章

超时证明没收到响应,不证明远端没做事。下图将副作用与回执分开,解释unknown状态、幂等标识与对账。仅给本地数据库加事务,不能自动消除远端重复执行风险。

下方图解的固定输入在“输入与适用条件”中声明。它用于解释本章机制,不与本章其他独立例子的数值暗中接续。

动态图解 · F33

超时告诉你没收到,不告诉你没发生

为什么自动重试有时会把一次工作做两遍?

外部服务可能已经完成动作,只是成功消息没回到调用方。此时“未知”是精确状态,不是系统不够聪明。对于会产生新记录、发送消息等副作用的动作,恢复需要稳定操作标识、结果核对或服务支持的幂等语义。只读查询的重试通常不需要同样复杂的设计。

幂等重复同一逻辑操作不产生额外副作用。
操作ID标识同一次逻辑动作,重试不随意换新ID。
事务在某个明确系统边界内保证一致性的机制。

输入与适用条件:构造的外部写入场景;不承诺跨数据库、文件系统和远程API天然具备“恰好一次”事务。

1 / 6 · 请求 op-1

第1步,共6步

窄屏可左右滑动图形,或点击“放大图形”。文字解释紧随图后。

超时告诉你没收到,不告诉你没发生:请求 op-1请求 op-1创建一条记录;客户端等待回执服务端提交记录已真实创建,副作用已发生回执丢失客户端超时,无法由此推断失败错误做法换新操作ID重试,可能创建第二条安全恢复使用稳定操作ID核对或幂等重试明确结果返回原结果,或确认未执行后再重试

创建一条记录;客户端等待回执

教学状态

默认静止。用“下一步”看完解释再继续;播放到最后一步自动停止。键盘聚焦播放器后,可用左右键逐步查看、空格播放/暂停。图中颜色辅助区分,含义同时用文字注明。

为什么需要

在不可靠网络下维持正确副作用次数。

怎样实现

先判断操作是否可重复,再决定重试、查状态、对账或人工确认。

可能的代价与误解

只在本地记一个成功标志,不能消除“远端已提交、本地未记录”的窗口。

如何判断是否做对

故意丢回执并重试,核对最终仅存在一条逻辑记录。

B.1|开始前,宿主拥有的四种对象

{
  "task_id":"t7",
  "goal":"修复exclude路径处理",
  "constraints":["按项目根目录相对路径匹配","保留包含规则","不提交、不部署"],
  "workspace":{"root":"/work/demo","revision":"r42"},
  "tools":["search_code","read_file","apply_patch","run_tests","inspect_operation"],
  "state":{"phase":"investigate","pending_calls":[],"verified":[]}
}

这不是全部直接塞给模型的唯一格式。任务对象、权限对象、工作区状态和工具注册表可以分开存储。模型输入只是从这些对象中选择和渲染的结果;执行器必须保留真实权限,不能以模型“说允许了”为准。

首轮尚无源码证据。主Agent可以先选择检索,而不是立刻猜一个补丁。若前端已附带相关源码,首轮也可能直接分析;本例选择无附件路线,为了展示完整检索循环。

B.2|模型调用M1:输入中为什么要包含工具Schema

{
  "instructions":"完成用户指定修复;用证据定位;遵守授权范围。",
  "user_task":"修复exclude路径处理。模式相对项目根目录;不改包含规则;不提交、不部署。",
  "workspace_hint":{"root":"/work/demo","revision":"r42"},
  "tools":[
    {"name":"search_code","arguments":{"query":"string","path_scope":"string"}},
    {"name":"read_file","arguments":{"path":"string","start_line":"integer","end_line":"integer"}}
  ],
  "history":[]
}

Schema告诉模型可以表达什么动作、参数叫什么。实际系统可能只暴露部分工具,其他工具通过发现机制按需加载。工具已经在注册表中,不表示每轮都必须把完整定义放进上下文。

M1输出不是最终答案,而是:

{"kind":"tool_call","call_id":"c1","name":"search_code",
 "arguments":{"query":"exclude","path_scope":"pipeline"}}

宿主解析响应项,检查工具是否存在、参数完整、调用范围允许,再执行。模型并没有在生成这个JSON的一刻直接读取磁盘。

B.3|工具T1:输出先成为证据,再进入下一轮

{"call_id":"c1","status":"succeeded","workspace_revision":"r42",
 "matches":[
  {"path":"pipeline/filter.py","lines":"20-35","preview":"def excluded(path, patterns): ..."},
  {"path":"tests/test_filter.py","lines":"10-28","preview":"test_relative_exclude ..."}
 ],"truncated":false}

返回片段只有定位作用,不足以确认完整控制流。若结果被截断,必须明确标注,不能把前几条当成全部。记录版本r42是为了后续判断证据是否仍对应当前工作区。

宿主将M1的工具调用与T1结果按call_id配对加入历史。模型M2至少需要看到原任务、约束、c1及其结果;可以还看当前计划,但不需要把工具内部日志全部复制进去。

B.4|模型调用M2:明确下一步缺哪段证据

M2根据定位结果发出两个读取动作:

[
 {"kind":"tool_call","call_id":"c2","name":"read_file",
  "arguments":{"path":"pipeline/filter.py","start_line":1,"end_line":70}},
 {"kind":"tool_call","call_id":"c3","name":"read_file",
  "arguments":{"path":"tests/test_filter.py","start_line":1,"end_line":60}}
]

它们是只读、互不依赖的操作,可以并行,但每个结果仍须保留自己的ID。c3先返回不代表它属于c2。宿主不应按“第一个回来就是第一个调用”匹配。

假设读到以下教学代码:

from fnmatch import fnmatchcase

def excluded(path, patterns):
    return any(fnmatchcase(path, pattern) for pattern in patterns)

def select_files(root, files, patterns):
    return [p for p in files if not excluded(str(p), patterns)]

这里还不能立刻断言root未使用就是唯一错误。需要确认files传入的是绝对路径还是相对路径,patterns语义是什么,路径分隔符规则如何。一个未使用参数是线索,不是根因证明。

B.5|模型调用M3:从候选解释到可反证检查

把需要验证的假设写成可检查关系:

H1:匹配函数本身不支持此模式。
H2:调用方把绝对路径传入了相对模式匹配。
H3:测试期望与已确定的产品语义不一致。

在本教学轨迹中,运行独立的两个断言:相对路径generated/a.py应匹配generated/*;同一文件绝对路径/work/demo/generated/a.py不匹配该相对模式。再确认用户指定的产品语义确实是根目录相对路径。这样H2获得支持,H1与H3被相应证据削弱。

M3输出运行测试调用c4,而不是“我已经验证过”:

{"kind":"tool_call","call_id":"c4","name":"run_tests",
 "arguments":{"target":"tests/test_filter.py","revision":"r42"}}

结果应包含命令、返回码、实际运行测试数和失败断言。这里的教学轨迹假定原始失败已被观察;不能把本页的示例文本当成真实仓库测试报告。可执行的机制实验见拓展阅读,确定性协议测试见计算与运行链路实验。两组示例分别说明输入条件和结果范围。

B.6|模型调用M4:补丁表达的是变更,不是结果

候选修复应在正确边界把路径转为根目录相对形式,并明确根外路径如何处理。relative_to可能失败;不能为了让它不报错就把异常静默吞掉,也不能无需求地重新设计所有路径语义。

{"kind":"tool_call","call_id":"c19","name":"apply_patch",
 "arguments":{
  "path":"pipeline/filter.py",
  "expected_revision":"r42",
  "patch_ref":"patch/p1",
  "purpose":"仅在匹配前构造根目录相对路径"
 }}

执行器检查版本和补丁前提,再修改文件。这里出现了三个不同状态:模型提出p1、执行器开始c19、文件实际变为r43。它们不能压成一个“已修复”。

如果其他用户先把文件改成r42b,expected_revision不匹配,应该拒绝或重新读取协调。直接按旧片段覆盖,会破坏用户修改。版本检查与写入之间还有并发窗口,需要与存储机制结合;仅在Python中“先读哈希再写”并不自动消除竞争。

B.7|最重要的故障窗口:文件改了,但成功结果丢失

假设c19已完成文件替换,运行器却在记录成功前崩溃。模型和任务状态只知道超时,外部文件已经是r43。

时间t1:记录 c19 pending
时间t2:执行 c19,文件 r42 → r43
时间t3:进程中断,成功响应未送达
时间t4:恢复时看到 c19 尚未确认

把c19标为failed并直接重试,可能再次修改文件;把它标为succeeded则可能在另一种故障窗口下撒谎。正确状态是unknown:需要核验。

恢复器调用inspect_operation或读取目标版本/补丁效果。如果外部系统支持幂等键,可以用c19或独立业务键查询;本地文件系统则检查目标内容和预期变更是否已应用。只有证据表明未发生,才允许按适当条件重试。

这里不能承诺“有一张本地表就实现分布式exactly-once”。如果状态表和外部副作用不在一个原子事务里,仍有窗口。工程目标是明确结果、避免危险重复,并为无法判定的情况提供受控恢复。

B.8|长会话要压缩:压缩前有哪些信息不能丢

到这里历史可能已有多次搜索、整文件读取和失败尝试。对后续任务最重要的是目标、约束、已确认根因、当前文件版本、未确认操作、测试状态和证据位置。

错误摘要:

已修复exclude问题,准备交付。

它同时犯了三件事:把执行未知写成完成;把原始测试结果误用到新版本;丢掉“不提交、不部署”。即使文字很短很流畅,也不适合作为恢复状态。

更合理的教学摘要:

{
 "task":"t7",
 "goal":"exclude按根目录相对路径匹配",
 "constraints":["保留包含规则","不提交、不部署"],
 "confirmed_cause":{"claim":"绝对路径进入相对模式匹配",
   "evidence":["e/read-c2@r42","e/test-c4@r42"]},
 "patch":{"id":"p1","call_id":"c19","status":"unknown"},
 "workspace":{"last_confirmed":"r42","observed_candidate":"r43"},
 "next":"先核验c19,再对确认后的当前版本运行测试",
 "verification":{"current_patch_tested":false}
}

摘要不是原始证据的替代品。e/read-c2@r42必须能找回来源;如果源码已变,要知道旧证据只能支持历史判断,不能直接代表当前事实。

B.9|压缩不是原地删历史,而是一次状态切换

先取得待压缩历史的快照版本H17,再生成候选摘要C18。校验关键约束、工具调用配对、unknown状态和证据引用。重建稳定系统指令、必要工具定义与新摘要,再条件性替换当前上下文。

若生成摘要时用户插入新指令,当前历史已变成H18,就不能盲目用基于H17的摘要覆盖新消息。可以把新消息追加到新上下文,或重新协调快照;具体策略由运行器决定。本质是并发状态更新问题,不是“让模型总结得更认真”就能解决。

压缩后模型请求M5应包含:原始授权范围、上述恢复状态、核验工具以及最新观察。旧的所有大段源码不必常驻,但模型需要时可以按证据ID重新读取。缓存命中可能变化;这影响成本,不应成为保留错误上下文的理由。

B.10|模型调用M5:先核验,不重复写

M5看到c19 unknown,选择:

{"kind":"tool_call","call_id":"c20","name":"inspect_operation",
 "arguments":{"operation_id":"c19","target":"pipeline/filter.py","patch_ref":"patch/p1"}}

教学返回:

{"call_id":"c20","status":"succeeded",
 "finding":{"operation_applied":true,"workspace_revision":"r43",
 "expected_change_present":true}}

宿主现在可以把c19更新为已核验完成,仍不能把整个任务标成完成。新代码是否满足功能和回归要求,是下一阶段验证问题。

如果返回只说“文件存在”,不足以证明补丁已应用;如果内容与p1部分一致,要识别部分修改、外部并发或核验不足,不能强行归到成功。结果Schema需要表达不确定性,而不是只有true/false逼模型选一个。

B.11|模型调用M6:测试必须绑定r43

M6调用测试c21,结果应表明实际在r43上运行了哪些测试。若通过,再检查改动范围和用户约束,形成最终交付。若测试期间源码再次变化,结果不能自动证明新版本正确,需要重新核对受影响范围。

最终回答的结构可以是:根因、修改范围、验证证据、剩余限制。不得声称已经提交或部署,因为用户明确禁止且本例没有执行。一个合格的结束条件是:所需修复已确认、相关验证完成、无待核验重要副作用、未越权。

这里不存在必须追加一整轮独立Review的固定要求。若变更风险低且证据充分,可结束;若跨模块语义或并发风险尚未确认,就继续有针对性的验证。Review的价值来自消除特定风险,不来自流程名字。

B.12|面试官继续追问:每轮都把全部历史重新传一遍吗

不一定。接口可能由客户端传完整逻辑历史,也可能引用服务端保存的响应或会话;服务仍需构建本轮可用上下文。逻辑历史连续、网络只发增量、计算缓存命中,是三个独立维度。

使用不透明压缩状态时,客户端可能无法逐字段检查模型内部保留内容,所以更要在外部持久状态中保留任务约束、操作结果和证据引用。不能把全部工程可靠性押在一个无法检查的摘要块上。

另一个追问是:子Agent是否自动继承这些工具和状态?不能假设。应根据产品和配置确认,交接时显式提供任务所需的最小证据与权限。主Agent有MCP,不代表每个子Agent都能访问;子Agent返回“完成”也不代表最终工作区已经整合并通过测试。

读完本例应能回答:一项事实最早在哪个阶段被知道、谁有权改变它、谁负责记录它、压缩后如何恢复,以及哪个证据足以关闭任务。能回答这些,才是理解Agent运行,而不只是会背“思考—行动—观察”。

记录保存在本机浏览器。

输入关键词即可检索。

↑ ↓ 选择 Enter 打开Esc 关闭 · 仅本地搜索

阅读设置

阅读设置仅保存在本机浏览器,不上传任何数据。