Agent 工程体系 · 第 52/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。
编程 Agent 工程:仓库上下文、搜索、补丁、命令、验证和提交
编程 Agent 不是“能生成代码的聊天机器人”,而是一个能够在真实仓库中观察状态、选择工具、修改文件、运行命令、解释失败并继续行动的软件系统。它的工作对象不是一段孤立的代码,而是一个带有目录结构、依赖关系、版本历史、测试约束、构建环境和权限边界的仓库状态。
OpenAI 对 Agent 的概括包括规划、调用工具、协作以及保留足够状态以完成多步任务;其运行时本质上是一个循环:调用模型、检查输出、执行工具调用、必要时切换 Agent,直到产生最终结果。(developers.openai.com) Anthropic 则区分了预定义的 workflow 与由模型动态决定下一步的 agent:前者强调可预测性,后者强调灵活性和模型驱动的决策。(anthropic.com)
这一区分对 Coding Agent 尤其重要:
- 如果任务是“执行固定的格式化、测试、构建流水线”,应该优先使用确定性的 workflow。
- 如果任务是“定位一个尚不明确的缺陷,并决定需要阅读哪些文件、运行哪些测试”,才需要 Agent 的动态决策能力。
- 实际系统通常是二者的组合:由 Agent 负责探索和修改,由工程代码负责权限、状态、命令策略和最终门禁。
本文将“编程 Agent 工程”具体化为一条可审计的数据流:
flowchart TD
U[用户任务] --> I[任务解析与约束提取]
I --> C[仓库上下文收集]
C --> S[搜索与证据定位]
S --> P[生成补丁]
P --> A[补丁预览与策略检查]
A -->|通过| W[应用到工作树]
A -->|拒绝| E[要求模型修正]
W --> X[执行命令]
X --> V[验证与失败诊断]
V -->|失败| S
V -->|通过| R[变更审查]
R --> G{是否允许提交}
G -->|否| O[输出补丁与验证报告]
G -->|是| T[创建 Git 提交]
T --> F[提交后确认与交付]
关键点不在于“模型写出了什么”,而在于每一步都产生了可检查的中间状态:模型看到了哪些文件,搜索依据是什么,补丁改了什么,命令返回了什么,验证覆盖了哪些性质,最后提交的对象是否正是验证过的工作树。
一、先定义对象:仓库、工作树、上下文和任务状态
1. 仓库不是目录,而是带历史的状态空间
在本文中,仓库是:
其中:
- :当前文件集合及其内容;
- :版本历史,例如 Git 提交、分支和标签;
- :依赖关系,包括模块、包、配置和生成文件之间的关系;
- :执行环境,例如操作系统、语言版本、依赖缓存和服务;
- :权限与策略,例如哪些文件可以修改、哪些命令需要审批。
Agent 实际操作的通常不是抽象仓库 ,而是一个工作树:
其中:
- 是当前磁盘上的文件;
- 是当前分支或提交基线;
- 是未提交变更,包括用户已有修改和 Agent 新增修改。
必须区分 与 。用户可能在 Agent 启动前已经修改了文件,或者另一个进程在运行期间改变了工作树。如果 Agent 直接执行 git add . && git commit,就可能把不属于本次任务的修改一起提交。
因此,一个合格的 Coding Agent 在开始前至少要记录:
pwd
git rev-parse --show-toplevel
git branch --show-current
git rev-parse HEAD
git status --short
git diff --stat
这些命令分别回答:
- 当前进程在哪个目录;
- 仓库根目录在哪里;
- 当前分支是什么;
- 任务开始时的基线提交是什么;
- 工作树是否干净;
- 当前已有变更的规模。
若启动时工作树不是干净的,系统应将任务划分为两个集合:
理论上,本次任务产生的变更应是:
但普通 Git 的差异并不总能可靠地区分两个集合,尤其当 Agent 修改了一个用户已经修改过的文件。因此,生产实现通常选择以下一种策略:
- 在独立 Git worktree 中执行任务;
- 为每个任务创建独立分支;
- 对用户已有修改直接拒绝自动修改;
- 为每个文件保存启动前快照并执行三方合并;
- 禁止自动提交,交由人工从差异中选择。
其中,独立 worktree 的价值不是“目录更干净”,而是把任务隔离转换为文件系统和 Git 引用层面的事实,而不是依赖模型记忆。
2. 仓库上下文不是把整个仓库塞进提示词
仓库上下文是 Agent 为完成当前任务而构造的、带来源和边界的证据集合:
其中:
- :用户任务及显式约束;
- :目标文件和符号;
- :搜索得到的相关代码片段;
- :项目规则、构建方式和测试约定;
- :命令结果、测试结果和历史证据。
“把整个仓库读入上下文”通常不是更可靠的方案,因为:
- 大量无关内容会稀释关键约束;
- 同名函数、旧实现和生成文件会增加误判;
- 模型无法自然地区分权威源文件与派生文件;
- 上下文越大,越难判断某条信息是否仍然有效。
正确的上下文收集过程是逐步扩大:
任务描述
→ 仓库入口与规则文件
→ 目标模块
→ 直接调用者与被调用者
→ 相关测试
→ 构建、类型和运行环境
→ Git 差异与历史
上下文还应保留来源。例如不要只传给模型:
函数 validate_user 负责校验用户。
而应传递类似结构:
{
"path": "src/user/validation.py",
"lines": "42-78",
"kind": "source",
"content": "...",
"reason": "由测试 test_invalid_email 直接引用"
}
这样做的原因是:模型的判断对象不只是代码内容,还包括“这段内容为什么被选中”。来源信息能够降低把测试注释、生成文件或旧文档误认为规范的风险。
二、搜索:不是查找字符串,而是构造证据链
1. 搜索的工程定义
搜索是从仓库状态中选择与任务相关证据的过程。设仓库中所有可检索对象为 ,任务为 ,搜索结果为:
其中 是相关性阈值。
但相关性并不只是文本相似度。一个对象可能在文本上高度匹配,却在语义上不相关。例如:
- 注释中出现了函数名,但代码路径不会执行;
dist/中有目标字符串,但它是构建产物;- 测试夹具中出现异常值,却不是生产规则;
- 旧迁移脚本中使用了同名字段,但当前版本已不再使用。
因此,Coding Agent 的搜索一般分为四层。
2. 第一层:结构搜索
结构搜索回答“代码在哪里”:
find . -maxdepth 2 -type f | sort
find src tests -type f | sort
随后排除明显的非源文件:
find . \
-path './.git' -prune -o \
-path './node_modules' -prune -o \
-path './dist' -prune -o \
-type f -print
结构搜索的结果不是最终上下文,而是建立候选空间。它能够告诉 Agent 项目是单体仓库、monorepo、前后端分离,还是由代码生成器维护的多层结构。
3. 第二层:符号和文本搜索
假设任务是“修复订单金额校验”,可以先搜索定义:
rg -n --glob '!dist/**' --glob '!vendor/**' \
'def validate_amount|function validateAmount|validate_amount|validateAmount' .
然后搜索调用者:
rg -n --glob '!dist/**' \
'validate_amount\(|validateAmount\(' src tests
最后搜索领域字段:
rg -n --glob '!dist/**' \
'amount|currency|decimal|round|precision' src tests
三次搜索产生不同类型的证据:
- 定义:规则在哪里实现;
- 调用者:规则在什么条件下生效;
- 领域字段:数值从哪里来、如何转换、如何展示。
只搜索定义而不搜索调用者,是常见误解。一个函数的正确修改范围取决于调用约定。例如把函数从“返回布尔值”改成“抛出异常”,定义本身可能看起来合理,但所有调用者都可能因此改变控制流。
4. 第三层:配置和规则搜索
编程 Agent 还必须搜索项目约束:
find .. -name 'AGENTS.md' -o -name 'CLAUDE.md' -o -name 'CONTRIBUTING.md'
find . -maxdepth 2 \
\( -name 'pyproject.toml' -o -name 'package.json' \
-o -name 'Makefile' -o -name 'Taskfile.yml' \
-o -name 'Dockerfile' \) -print
这些文件可能定义:
- 测试入口;
- 格式化和静态检查命令;
- 禁止修改的生成目录;
- 依赖安装方式;
- 提交信息格式;
- 特定目录的额外规则。
规则具有作用域。仓库根目录中的规则通常影响整个仓库,而子目录中的规则只适用于该目录及其后代。Agent 不应把一条局部规则错误地扩展到整个仓库,也不应忽略更具体的局部规则。
这正是 Agent Skills 中“指令作用域”的工程意义:技能不只是提示词,而是对触发条件、可用资源、工具和版本的治理。一个“数据库迁移 Skill”若能在所有任务中自动触发,反而会把不相关任务引导到高风险工具路径。
5. 第四层:历史搜索
当当前代码无法解释行为时,再搜索 Git 历史:
git log --oneline -- src/orders
git blame -L 42,78 -- src/orders/validation.py
git log -S 'validate_amount' --all --oneline
历史证据可以回答:
- 某个判断为什么存在;
- 某个测试是否曾经防止过回归;
- 当前行为是设计约定还是临时修复;
- 一个看似冗余的分支是否服务于兼容性。
但历史不是自动的规范。旧提交可能对应不同依赖、不同业务规则或已废弃接口。因此历史搜索的输出应被标记为“背景证据”,不能直接覆盖当前测试、当前文档和当前接口契约。
三、补丁:让模型提出变更,让系统决定能否应用
1. 什么是补丁
补丁是对工作树的受限变换:
其中 描述新增、修改或删除哪些文件以及如何变化。
补丁与“让模型输出完整文件”有本质区别:
- 完整文件输出容易覆盖模型没有读到的内容;
- 补丁可以被逐行审查;
- 补丁应用失败时能够精确反馈冲突位置;
- 补丁天然适合与 Git diff、代码审查和回滚结合。
OpenAI 的 Apply Patch 工具将文件操作划分为创建、更新和删除,并要求宿主环境应用补丁、记录成功或失败,再把结果反馈给模型继续处理。(developers.openai.com) 这体现了一个重要边界:模型提出补丁,执行环境负责落盘。
2. 补丁应用不是字符串替换
一个可靠的补丁应用器至少应检查:
- 路径是否位于仓库根目录内;
- 是否尝试访问符号链接指向的仓库外路径;
- 目标文件是否仍与模型读取时一致;
- 上下文行是否匹配;
- 是否触碰禁止目录;
- 删除或重命名是否需要审批;
- 文件编码和换行符是否可接受。
路径检查可以抽象为:
不能只检查字符串前缀。例如:
/home/agent/repo/file
/home/agent/repo-other/file
简单的 startswith("/home/agent/repo") 会错误放行第二条路径。正确方式是对规范化后的路径使用路径组件比较。
补丁还应绑定读取版本。设模型读取文件时的哈希为 ,应用前工作树文件哈希为 :
否则模型可能基于旧内容覆盖了用户刚刚写入的修复。
3. 一个最小补丁示例
假设原文件为:
def normalize_name(name: str) -> str:
return name.strip().lower()
任务要求保留大小写,只删除两端空白。补丁应明确表达意图:
diff --git a/src/name.py b/src/name.py
index 9d2a1ab..52f4e81 100644
--- a/src/name.py
+++ b/src/name.py
@@ -1,2 +1,2 @@
def normalize_name(name: str) -> str:
- return name.strip().lower()
+ return name.strip()
应用后必须立即检查:
git diff --check
git diff -- src/name.py
git diff --check 只能检查部分格式问题,不能证明语义正确;git diff 只能证明文件发生了预期变化,不能证明所有调用者都兼容。因此补丁应用完成只是“工作树发生了受控变化”,不是任务完成。
四、命令:让 Agent 观察真实环境,而不是猜测结果
1. 命令工具的定义
命令是 Agent 请求执行环境完成一个外部操作的工具调用,例如搜索文件、运行测试、构建项目或检查 Git 状态。
命令执行结果至少应包含:
其中:
- :实际执行的命令;
- :标准输出;
- :标准错误;
- :退出码;
- :是否超时;
- :执行时的工作目录、环境和资源限制。
只返回一段合并后的文本是不够的。模型必须知道“命令失败”与“命令成功但输出包含警告”的区别。
OpenAI 的 Shell 工具支持托管容器和本地运行时,并明确建议命令执行非交互、保留非零退出结果,在超时时返回部分输出。(developers.openai.com)
2. 命令必须有生命周期
一次命令调用的生命周期可以表示为:
请求
→ 参数校验
→ 策略匹配
→ 审批(必要时)
→ 创建进程组
→ 设置工作目录和环境
→ 捕获 stdout/stderr
→ 处理超时或信号
→ 回收子进程
→ 返回结构化结果
以下是一个框架无关的 Python 最小实现:
from dataclasses import dataclass
import os
import signal
import subprocess
from pathlib import Path
@dataclass
class CommandResult:
command: str
cwd: str
stdout: str
stderr: str
exit_code: int | None
timed_out: bool
def run_command(command: str, cwd: str, timeout: int = 60) -> CommandResult:
root = Path(cwd).resolve()
process = subprocess.Popen(
["sh", "-c", command],
cwd=root,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
start_new_session=True,
env={
"PATH": os.environ["PATH"],
"HOME": str(root / ".agent-home"),
"CI": "1",
},
)
try:
stdout, stderr = process.communicate(timeout=timeout)
return CommandResult(
command=command,
cwd=str(root),
stdout=stdout,
stderr=stderr,
exit_code=process.returncode,
timed_out=False,
)
except subprocess.TimeoutExpired as exc:
os.killpg(process.pid, signal.SIGTERM)
stdout, stderr = process.communicate()
return CommandResult(
command=command,
cwd=str(root),
stdout=(exc.stdout or "") + stdout,
stderr=(exc.stderr or "") + stderr,
exit_code=None,
timed_out=True,
)
这个实现只是教学用最小版本,生产环境仍需处理:
- 命令注入;
- 资源限制;
- 进程逃逸;
- 文件系统访问;
- 网络访问;
- 输出大小;
- 子进程回收;
- 密钥泄露;
- 平台差异。
尤其不能把模型提供的字符串直接拼接到宿主机 Shell 中。命令工具应将“模型意图”和“执行格式”分离。例如为测试提供:
{
"operation": "run_tests",
"target": "tests/unit/test_user.py"
}
由宿主程序映射为固定命令:
python -m pytest tests/unit/test_user.py
相比允许模型任意生成 sh -c,这种方式牺牲了一部分灵活性,却显著降低了高风险操作面。
3. 命令结果如何反馈
错误结果不能只说“失败了”。应返回足够诊断的信息:
{
"command": "python -m pytest tests/unit/test_user.py",
"exit_code": 1,
"timed_out": false,
"stdout_tail": "....F",
"stderr_tail": "AssertionError: expected 3, got 2",
"cwd": "/workspace/repo",
"changed_files": ["src/user.py"],
"retryable": true
}
其中 retryable 不应由模型猜测,而应由命令分类器或策略层给出:
- 编译错误:通常可修复并重试;
- 测试断言失败:可能需要修改代码;
- 依赖下载失败:可能是环境或网络问题;
- 权限拒绝:通常不应盲目重试;
- 超时:需要缩小测试范围或调整资源限制;
- 发现未提交用户修改:应暂停任务。
五、验证:证明代码满足性质,而不是证明命令执行过
1. 验证的层次
验证是根据任务契约和项目约束,判断当前工作树是否满足可接受条件。
设任务要求为 ,当前工作树为 ,验证器为 :
pass 不是“没有看到错误”,而是所有要求的检查都通过。fail 表示至少一个性质被违反。unknown 表示检查没有完成,例如超时、依赖缺失或环境不可用。
典型验证层次如下:
- 补丁完整性:是否有空白错误、未预期文件、非法路径;
- 语法和类型:代码能否解析、编译或通过类型检查;
- 单元测试:局部行为是否符合预期;
- 集成测试:模块间接口是否兼容;
- 构建验证:生产构建是否成功;
- 回归验证:修改是否破坏相关旧行为;
- 安全验证:是否引入危险依赖、权限绕过或敏感信息;
- 交付验证:提交内容是否与验证内容一致。
验证顺序通常从便宜、局部的检查开始,再逐步扩大范围:
git diff --check
python -m compileall src
python -m pytest tests/unit/test_user.py
python -m pytest
python -m build
但顺序不是固定规范。一个前端项目可能先执行类型检查和 lint,一个数据库迁移任务可能先执行 schema 验证。关键是每个命令都应对应一个明确性质。
2. 测试失败的诊断循环
假设 Agent 修改后运行:
$ python -m pytest tests/unit/test_amount.py
1 failed, 8 passed
E assert Decimal('10.00') == Decimal('9.99')
正确的下一步不是立即再改一遍,而是建立失败分类:
- 测试本身错误:预期值不再符合当前规范;
- 实现错误:计算或舍入规则错误;
- 上下文不完整:Agent 没有读取货币精度配置;
- 环境错误:依赖版本或区域设置不同;
- 非确定性失败:时间、随机数、并发或外部服务造成。
然后收集证据:
sed -n '1,220p' tests/unit/test_amount.py
rg -n 'Decimal|round|precision|currency' src tests
git diff
git log -S '9.99' --all --oneline
如果模型只根据一条断言失败就修改测试,可能把正确实现改成错误行为。验证结果必须反过来驱动搜索,而不是简单驱动下一次生成。
3. 验证不是越多越好
完整测试并不总是最优选择。设一次验证的收益为 ,成本为 ,则可以把检查选择近似为:
其中:
- 表示是否执行第 个验证;
- 是该验证发现本类错误的概率或价值;
- 是执行成本;
- 反映延迟和资源约束。
局部修改先运行局部测试通常合理,但当修改跨越公共接口、序列化格式、数据库 schema 或构建配置时,局部测试通过不能作为最终证据。验证范围必须跟随变更影响面扩大。
六、提交:Git 操作是交付边界,不是格式化动作
1. 提交的含义
提交是把经过选择的工作树差异写入 Git 历史:
其中:
- :被选择提交的差异;
- :提交信息;
- :作者和提交元数据。
提交具有不可忽略的副作用:它改变共享历史、触发 CI、触发部署或通知。因此“修改文件”和“提交代码”应属于不同风险等级。
推荐的状态机是:
EDITING
→ PATCHED
→ VALIDATING
→ VALIDATED
→ REVIEW_REQUIRED
→ COMMITTED
其中只有 VALIDATED 状态允许进入提交前检查,而且提交前必须重新确认工作树没有发生漂移:
git status --short
git diff --name-only
git diff --check
git diff --cached --check
git diff --cached
git rev-parse HEAD
2. 不要使用 git add .
git add . 的问题不是命令本身错误,而是它的选择范围通常大于任务范围。更安全的方式是先获得允许提交的文件集合:
git add -- src/user.py tests/unit/test_user.py
git diff --cached --name-status
git diff --cached --check
如果系统要求精确到行级别,可以使用交互式暂存:
git add -p
但自动化 Agent 不应假设交互式命令一定可用。命令工具应是非交互的;OpenAI 的 Shell 文档也明确要求不要依赖交互式命令。(developers.openai.com)
3. 提交前必须验证“被提交的对象”
一个典型错误是:
- 工作树验证通过;
- Agent 又修改了一个文件;
- 只暂存了部分文件;
- 提交内容与验证内容不一致。
因此验证对象应明确区分:
W:完整工作树;Δ:本次任务差异;I:暂存区;C:最终提交。
最终应满足:
并且:
如果不能证明这两个条件,就不应自动提交。
提交命令可以是:
git commit -m "fix: preserve input name casing"
提交后还要确认:
git show --stat --oneline HEAD
git status --short
git log -1 --format='%H%n%s'
预期结果包括:
HEAD是刚创建的提交;- 提交文件只包含允许范围;
- 工作树状态符合策略;
- 提交信息能够说明行为变化,而不是只写“update code”。
4. 自动提交的边界
以下任务通常可以允许自动提交:
- 运行在隔离分支或独立 worktree;
- 变更范围有限;
- 所有验证通过;
- 提交不触发生产发布;
- 提交信息由固定规则生成;
- 没有用户已有修改;
- 没有删除、迁移、权限或依赖升级等高风险操作。
以下情况应暂停并要求人工确认:
- 修改认证、授权、支付、数据库迁移;
- 删除文件或大范围重命名;
- 更改 CI、部署、容器或密钥配置;
- 运行需要网络、写外部系统或发布的命令;
- 测试失败但模型声称“可以忽略”;
- 工作树在执行期间发生外部变化;
- Agent 无法区分用户修改和自身修改。
OpenAI 的 Agent SDK 将 guardrail 定义为自动检查,将 human review 定义为在敏感动作前暂停并由人或策略批准;二者共同决定运行继续、暂停还是停止。(developers.openai.com)
七、沙箱:把命令风险限制在执行边界内
1. 沙箱解决什么问题
代码执行沙箱是为 Agent 提供受隔离约束的进程、文件、网络和资源环境。它解决的不是“模型会不会犯错”,而是“模型犯错时,错误影响能否被限制”。
OpenAI 的 Sandbox Agent 文档将沙箱会话描述为拥有文件、命令、端口和供应商隔离状态的执行环境,并区分了 Agent 定义、manifest、sandbox client、session、运行配置和保存状态。(developers.openai.com)
至少应分别设计以下边界:
- 进程:是否允许创建子进程,如何限制进程组和信号;
- 容器:是否使用容器、虚拟机或宿主机本地执行;
- 文件:可读写目录、挂载点、符号链接和敏感文件;
- 网络:是否允许出站访问,允许哪些域名和端口;
- 资源:CPU、内存、磁盘、进程数、输出大小和执行时间;
- 销毁:任务完成、失败或超时后如何回收会话和临时文件。
2. 沙箱并不等于安全
沙箱可能仍然存在:
- 容器内权限过高;
- 宿主目录错误挂载;
- Docker socket 暴露;
- 网络访问导致数据外传;
- 依赖安装执行恶意脚本;
- 日志中泄露环境变量;
- 临时目录和缓存未销毁;
- 多个任务复用同一工作空间造成交叉污染。
因此安全模型应是:
缺少任意一项都可能使整体边界失效。
3. 持久状态与会话状态必须分离
一个长任务可能需要暂停、人工审核后继续,或在多次运行之间保留工作成果。这时应区分:
- 对话状态:模型消息和工具调用历史;
- 运行状态:当前 Agent、审批状态、工具状态;
- 沙箱会话状态:正在运行的执行环境;
- 工作区快照:某一时刻的文件内容;
- 记忆文件:供后续任务读取的经验或产物。
OpenAI 的沙箱文档明确区分了 RunState、序列化 session state 和 snapshot,并给出了恢复顺序:优先复用活动会话,其次恢复运行状态或序列化会话,最后才创建新会话。(developers.openai.com)
这里有一个常见误解:保存了对话历史,不代表保存了文件系统。模型知道“上次改过 src/app.py”,并不意味着下一次运行中的沙箱还存在该文件。要恢复工作,必须同时持久化或重新构造工作区。
八、一个完整的 Coding Agent 控制循环
下面给出与具体模型和 SDK 无关的伪代码。它展示的是工程生命周期,而不是某个厂商的固定 API:
def coding_agent(task, repo):
baseline = snapshot_repo(repo)
context = inspect_repository(
repo,
include_rules=True,
include_status=True,
include_manifest=True,
)
plan = model.plan(task=task, context=context)
while True:
evidence = search_repository(
repo,
queries=plan.search_queries,
exclude_generated=True,
)
patch = model.propose_patch(
task=task,
context=context,
evidence=evidence,
constraints=plan.constraints,
)
check_patch_policy(patch, repo)
apply_patch(repo, patch)
command_results = []
for check in choose_checks(task, patch, repo):
result = run_in_sandbox(
command=check.command,
cwd=repo,
timeout=check.timeout,
)
command_results.append(result)
if result.timed_out or result.exit_code not in (0,):
diagnosis = model.diagnose(
task=task,
patch=patch,
result=result,
diff=current_diff(repo),
)
if diagnosis.stop:
return report_failure(baseline, repo, diagnosis)
plan = diagnosis.next_plan
break
else:
if human_approval_required(task, patch):
return request_review(baseline, repo, command_results)
if not commit_policy_allows(task, patch, command_results):
return report_success_without_commit(
baseline, repo, command_results
)
commit_files = select_allowed_files(baseline, repo)
verify_commit_boundary(commit_files, command_results)
commit(repo, commit_files, message=make_commit_message(task))
return report_success(baseline, repo, command_results)
这个循环有四个重要性质。
第一,模型不能跳过仓库检查。即使用户说“只改一行”,Agent 也必须确认目标文件、规则和已有修改。
第二,补丁应用和命令执行是两个阶段。补丁成功不代表程序正确,命令成功也不代表补丁范围正确。
第三,失败会回到搜索和诊断,而不是简单重复相同调用。重复同一动作只会增加成本,不会增加信息。
第四,提交是最后的策略决策,不是工具链的自然终点。某些任务应在验证通过后只输出补丁,由人工完成提交。
九、完整算例:修复一个用户名称规范化问题
假设用户提出:
修复用户名称保存时被强制转为小写的问题,并补充回归测试。
第一步:建立初始状态
git rev-parse HEAD
git status --short
rg -n 'normalize_name|lower\(\)|username|display_name' src tests
假设结果发现:
src/user/name.py:3:def normalize_name(name: str) -> str:
src/user/name.py:4: return name.strip().lower()
src/user/service.py:18:normalized = normalize_name(request.display_name)
tests/unit/test_name.py:7:def test_normalize_name():
此时还不能马上删除 .lower(),因为需要确定:
display_name是否应该保留大小写;username是否仍然要求小写;- 数据库存储字段是否共用同一个规范化函数;
- 测试是否把两个概念混在了一起。
第二步:读取定义、调用者和测试
sed -n '1,120p' src/user/name.py
sed -n '1,160p' src/user/service.py
sed -n '1,160p' tests/unit/test_name.py
rg -n 'display_name|username' src tests
假设证据显示:
def normalize_name(name: str) -> str:
return name.strip().lower()
而测试为:
def test_normalize_name():
assert normalize_name(" Alice ") == "alice"
这说明函数名过于宽泛,当前测试只证明旧行为,不足以证明业务需求。Agent 应先区分两个概念:
def normalize_display_name(name: str) -> str:
return name.strip()
def normalize_username(name: str) -> str:
return name.strip().lower()
如果调用者确实把 display_name 传给了原函数,则补丁应修改调用路径,并保留用户名路径的大小写折叠。
第三步:生成最小补丁
diff --git a/src/user/name.py b/src/user/name.py
@@
-def normalize_name(name: str) -> str:
- return name.strip().lower()
+def normalize_display_name(name: str) -> str:
+ return name.strip()
+
+def normalize_username(name: str) -> str:
+ return name.strip().lower()
调用者:
diff --git a/src/user/service.py b/src/user/service.py
@@
-normalized = normalize_name(request.display_name)
+normalized = normalize_display_name(request.display_name)
测试:
diff --git a/tests/unit/test_name.py b/tests/unit/test_name.py
@@
-def test_normalize_name():
- assert normalize_name(" Alice ") == "alice"
+def test_normalize_display_name_preserves_case():
+ assert normalize_display_name(" Alice ") == "Alice"
+
+def test_normalize_username_lowercases():
+ assert normalize_username(" Alice ") == "alice"
第四步:应用并验证
git diff --check
python -m pytest tests/unit/test_name.py
python -m pytest tests/unit/test_user_service.py
python -m pytest
如果全量测试失败,不能直接判断是补丁错误。例如某个测试可能仍然导入旧符号:
ImportError: cannot import name 'normalize_name'
此时 Agent 需要搜索所有引用:
rg -n 'normalize_name' .
如果确认这是公开模块 API,则不能随意删除旧函数;可以保留兼容包装:
def normalize_name(name: str) -> str:
"""Backward-compatible username normalization."""
return normalize_username(name)
这就是上下文对补丁的约束作用:局部修复并不自动授权破坏公共接口。
第五步:提交前检查
git status --short
git diff --name-only
git diff -- src/user/name.py src/user/service.py tests/unit/test_name.py
确认没有额外文件后:
git add -- \
src/user/name.py \
src/user/service.py \
tests/unit/test_name.py
git diff --cached --check
python -m pytest
git diff --cached --name-status
git commit -m "fix: preserve display name casing"
提交前重新运行全量测试,是为了确认暂存区中的内容和此前验证的工作树一致。若中间又有修改,应取消提交流程并重新验证。
十、典型失败路径与诊断方法
1. 上下文不足导致“看似合理”的错误
表现:Agent 修改了目标函数,但遗漏了生成代码、配置映射或另一个调用者。
诊断:
rg -n '目标符号|字段名|接口路径' .
git diff --name-only
原因:搜索只覆盖定义,没有覆盖引用和配置。
恢复:以当前补丁中的每个改动符号为新查询,重新寻找调用者、测试和接口边界。
2. 补丁上下文冲突
表现:补丁无法应用,或者应用后差异异常大。
原因:
- 文件在模型读取后发生变化;
- 行号依赖过强;
- 目标文件包含自动生成内容;
- 换行符或格式化工具重写了文件。
恢复:
git diff
git status --short
git hash-object path/to/file
重新读取目标区域,生成更小的补丁,而不是强制覆盖整个文件。
3. 命令成功但结论错误
表现:pytest 返回 0,但功能仍然错误。
原因:
- 测试没有覆盖任务场景;
- 运行了错误的 Python 环境;
- 测试被跳过;
- 只运行了旧测试;
- 测试断言过弱。
诊断:
python --version
python -m pytest -ra
python -m pytest --collect-only
git diff --stat
命令退出码只是一个信号,不是完整证明。验证设计必须确认“测试确实执行了相关断言”。
4. 沙箱泄露或污染
表现:不同任务看到彼此文件,或者任务能够读取宿主机密钥。
原因:
- 复用了同一个持久会话;
- 挂载范围过大;
- 环境变量全部继承;
- 任务完成后没有销毁临时目录;
- 网络出口没有策略控制。
恢复:
- 停止并销毁当前会话;
- 检查挂载、环境和网络日志;
- 轮换可能暴露的密钥;
- 为每个任务创建全新工作空间;
- 仅恢复经过审核的快照,而不是整个会话。
十一、工程边界:哪些应交给模型,哪些必须由系统掌控
适合交给模型的内容:
- 根据任务决定先搜索哪些符号;
- 从多个文件中推断调用关系;
- 解释测试失败的可能原因;
- 生成小范围补丁;
- 选择下一步诊断动作;
- 形成变更说明。
不应只交给模型决定的内容:
- 是否能访问某个目录;
- 是否允许执行任意 Shell;
- 是否允许出站网络;
- 是否能删除文件;
- 是否能修改 CI、部署和密钥配置;
- 是否能提交或推送;
- 是否可以忽略测试失败;
- 是否把用户已有修改纳入提交。
这不是“信任模型”与“不信任模型”的二元问题,而是职责分离问题。模型适合处理不确定的语义搜索和计划选择;宿主系统适合处理确定的安全策略、状态转换、资源回收和审计。
从框架角度看,Agent SDK 的运行循环、工具、handoff、审批、状态和沙箱都属于可组合运行时能力;但这些能力并不会自动替代仓库策略。(developers.openai.com)
十二、判断一个编程 Agent 是否可靠
可靠性不能用“生成代码速度”衡量,而应检查以下不变量:
如果一个系统只能“让模型编辑文件”,却不能回答“模型依据哪些上下文编辑、补丁是否冲突、命令在哪里执行、测试是否真的覆盖、提交包含哪些文件”,它还不是完整的编程 Agent 工程。
真正可维护的 Coding Agent,应把仓库上下文、搜索、补丁、命令、验证和提交组织成一条具有明确状态和证据的闭环:模型负责在不确定性中提出行动,工具负责产生事实,策略层负责限制副作用,验证器负责判断性质,Git 负责记录最终交付。
系列导航与关联阅读
- 系列入口:Agent 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:LlamaIndex Agent:Workflow、Tool、Context、RAG 和多 Agent
- 下一篇:Agent Skills:触发条件、指令作用域、资源、工具和版本治理
- 延伸:Agent 代码执行沙箱:进程、容器、文件、网络、资源和销毁
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论