Code Agent 与计算机使用
产出最高的 Agent,往往是那些能操作真实环境的:改代码库、跑测试、点网页、操作桌面。它们有天然的验证器(测试能跑、页面状态可查)、可回滚、可审计,因此最早进入生产;但也因为「真的执行、真的触达外部系统」而成为风险最高的类别。本页讲两类主流形态——Code Agent(仓库内写码)与计算机使用(Computer Use)(浏览器/桌面操作)——的共同架构、关键工程、沙箱与失败模式;通用控制循环见 Agent 架构通论,安全红线展开见 Agent 安全与治理。
一、为什么它们是 Agent 的主战场
| 特征 | Code Agent | Computer Use(浏览器/桌面) |
|---|---|---|
| 任务示例 | 修 issue、加功能、代码审查、重构 | 填表、查资料、管理后台、桌面操作 |
| 环境验证 | 测试套件 / 编译 / linter | 页面状态 / 元素断言 / 截图对比 |
| 可回滚性 | git + 沙箱分支,强 | 快照回滚较弱,副作用多(下单/发信) |
| 主要风险 | 改坏别处、装恶意依赖 | 误操作真实服务、注入逃逸 |
- 两者共享同一条主干:感知环境 → 决定动作 → 执行 → 用环境反馈修正,区别只在「动作空间与验证方式」;
- 术语现状提示:此领域模型与工具演进极快,本文讲不随版本变的工程方法;具体模型能力/官方工具的现状请查各自官方文档与最新基准。
二、Code Agent:仓库内的写码循环
工作流全景
读任务(issue/需求) → 理解仓库(结构/符号/相关文件) → 定位修改点
→ 小步编辑(带 diff) → 跑相关测试 → 看失败原因回填 → 自修复(有预算!)
→ 全量验证 → 交 patch/提 PR(附测试证据)关键工程点
- 仓库理解 ≠ 把整个 repo 塞上下文:
- 第一遍先看:文件树、README、入口/测试目录 → 由 检索 定位相关文件与符号;
- 维护「repo map」级别信息(模块职责、构建命令、测试命令)存外部,别每轮重读;
- 编辑要小步、可检查:每次改动控制在能 review 的规模;改后先跑相关测试,再决定是否扩大;
- 验证循环是灵魂:
- 按改动相关性选测试(全量测试很贵);flaky 测试单独标记,别让偶发失败触发无意义自修复;
- 自修复要有预算:同一失败最多重试 2-3 次,每次用真实的错误输出回填,而不是猜;
- 「测试全绿」≠「任务完成」:测试可能是 Agent 自己写的宽松断言——要独立评审测试质量、看覆盖率,或让另一个 agent/人 review;
- 改完交差前必须:跑你改动相关的测试 + lint/编译;留下「我改了哪些文件、为什么、验证了什么」的摘要(可读性对 review 与人机协作至关重要);
- 环境隔离:在独立分支/沙箱里改与跑,别直接动共享环境;依赖变更(如
pip install)先声后行。
演示:测试驱动的真实修复闭环(deepseek-chat, 2026-09-04)
构造一个带缺陷的
mini-billing仓库(billing.py计税顺序写错 + 2 个测试,基线 pytest 失败),只给 Code Agent 四个工具list_dir / read_file / write_file / run_tests,路径全部限制在仓库内。结果:Agent 依次「看目录 → 读被测函数 → 读测试 → 最小修复 → 跑测试全绿 → 口头宣称完成」,程序复核测试通过后才算完成。运行:设置BUG_REPO指向被测仓库后export DEEPSEEK_API_KEY=sk-... && python3 本文件(演示时 6313 tokens)。
python
# Code Agent 最小闭环 —— 真实可跑版(DeepSeek deepseek-chat)
# 依赖: python3 + requests + pytest; 环境变量 DEEPSEEK_API_KEY
import os, json, subprocess, requests
API = "https://api.deepseek.com/chat/completions"
MODEL = os.environ.get("DEEPSEEK_MODEL", "deepseek-chat")
REPO = os.environ.get("BUG_REPO", ".") # 沙箱根: 指向被测仓库(演示用 .demo_codeagent_tmp/repo)
PYTHON = os.environ.get("PYTEST_PYTHON", "python3")
def _in_repo(path):
root = os.path.realpath(REPO)
p = os.path.realpath(os.path.join(REPO, path))
return p if p == root or p.startswith(root + os.sep) else None # 越界即拒绝
def exec_tool(name, args):
if name == "list_dir":
p = _in_repo(args.get("path", "."))
return {"ok": True, "files": sorted(os.listdir(p))} if p else {"ok": False, "error": "越界"}
if name == "read_file":
p = _in_repo(args["path"])
if not p:
return {"ok": False, "error": "越界"}
try:
return {"ok": True, "content": open(p, encoding="utf-8").read()[:3000]}
except Exception as e:
return {"ok": False, "error": str(e)}
if name == "write_file":
p = _in_repo(args["path"])
if not p:
return {"ok": False, "error": "越界"}
try:
os.makedirs(os.path.dirname(p), exist_ok=True)
open(p, "w", encoding="utf-8").write(args["content"])
return {"ok": True}
except Exception as e:
return {"ok": False, "error": str(e)}
if name == "run_tests": # 铁律: 完成 = 相关测试实跑过
r = subprocess.run([PYTHON, "-m", "pytest", "-q"], cwd=REPO, capture_output=True,
text=True, timeout=120)
return {"ok": True, "passed": r.returncode == 0, "tail": (r.stdout + r.stderr)[-900:]}
return {"ok": False, "error": "unknown tool"}
TOOLS = [{"type": "function", "function": {"name": n, "description": d,
"parameters": {"type": "object", "properties": p, "required": r}}}
for n, d, p, r in [
("list_dir", "列出仓库某目录下的文件名(相对仓库根)。", {"path": {"type": "string"}}, []),
("read_file", "读取仓库内文本文件内容。", {"path": {"type": "string"}}, ["path"]),
("write_file", "整文件覆盖改写/新建仓库内文件。", {"path": {"type": "string"}, "content": {"type": "string"}}, ["path", "content"]),
("run_tests", "在仓库内运行全部 pytest, 返回是否通过与输出尾部。", {}, []),
]]
def chat(messages):
payload = {"model": MODEL, "messages": messages, "tools": TOOLS, "stream": False}
r = requests.post(API, headers={"Authorization": "Bearer " + os.environ["DEEPSEEK_API_KEY"]},
json=payload, timeout=90)
r.raise_for_status()
body = r.json()
return body["choices"][0]["message"], body["usage"]["total_tokens"]
def run(goal, max_steps=10):
history = [{"role": "user", "content": goal + ("\n规则: 先用工具看清结构与失败点再改; 修复最小化; "
"改完必须 run_tests 验证, 全绿才可宣告完成; run_tests 失败要读返回错误再改。")}]
tokens = 0
for _ in range(max_steps):
msg, t = chat(history)
tokens += t
history.append(msg)
if msg.get("tool_calls"):
for tc in msg["tool_calls"]:
args = json.loads(tc["function"]["arguments"] or "{}")
result = exec_tool(tc["function"]["name"], args)
history.append({"role": "tool", "tool_call_id": tc["id"],
"content": json.dumps(result, ensure_ascii=False)})
else:
# 模型口头说完成: 不信, 程序化复核 = 真实再跑一遍测试
check = exec_tool("run_tests", {})
if check.get("passed"):
return True, "validated(测试实际全绿)", tokens
history.append({"role": "user", "content": "测试仍未通过, 依据测试输出继续修复。"})
return False, "step_budget_exhausted", tokens
if __name__ == "__main__":
assert os.environ.get("DEEPSEEK_API_KEY") and os.path.isdir(REPO)
ok, reason, tokens = run("仓库内测试当前失败, 请定位缺陷并修复, 使全部 pytest 通过。")
print("done:", ok, "| reason:", reason, "| tokens:", tokens)
# ---- 实测轨迹摘要(2026-09-04, 目标= mini-billing 仓库) ----
# GOAL -> list_dir(/) -> read_file(billing.py) -> list_dir(tests) -> read_file(test_billing.py)
# -> write_file(billing.py, 修复) -> run_tests(通过, 2 passed)
# -> CLAIM「所有测试均通过」 -> 复核 run_tests -> validated(测试实际全绿) | tokens=6313
# 初始基线: pytest 失败 F. assert 90.5 == 94.5 ← 税被错误乘到折扣额 discount 上- 与上文对照的三件事都真实发生:Agent 先侦察再改(没乱扫就动手)、修复最小化(只改一行:把税乘的对象从
discount改回subtotal)、口头完成不算数(模型宣称通过后脚本又真实跑了一次测试才置done)。
Code Agent 典型失败模式
| 症状 | 根因 | 对策 |
|---|---|---|
| 改了 A 处破坏 B 处 | 只看了局部,没查引用 | 改动前找引用(lsp 式符号分析);加回归测试 |
| 测试全绿但需求没实现 | 测试写得太松/覆盖不到需求 | 独立评审测试;从需求抽「验收断言」而非自写用例 |
| 无限自修复死循环 | 没有预算或没有真实错误信息 | 重试预算 + 用真实报错回填 |
| 乱装依赖/改坏环境 | 环境不隔离、依赖不经声明 | 沙箱分支、依赖变更显式审批 |
| 找错文件/改错层 | 仓库理解不足就动手 | 先定位(检索/符号),输出「修改计划」再编辑 |
| 断言「完成」但没跑测试 | 验证是口头不是执行 | 铁律:完成 = 相关测试实际跑过 |
三、计算机使用(Computer Use / 浏览器 Agent)
形态与感知
- 两类感知路线:
- 可访问性树/DOM:浏览器可读页面结构与状态,精确、省 token;
- 截图视觉:看真实渲染(桌面/被遮元素/JS 渲染),通用但费、易误读;
- 多数实现走「先结构化(DOM/AT),不够再上视觉」;
- 动作空间:点击/输入/滚动/导航/表单提交/读页面——每次动作后要「观察页面变化」再决定下一步(页面是异步渲染,需等待稳定);
- 状态化问题突出:登录态、会话、多标签、弹窗/iframe——复用 记忆与状态 的状态结构管理「当前页面任务进度」。
关键工程点
- 验证在页面内完成:任务的完成判断要可程序化——元素出现、提交成功页、URL 变化,而不是 Agent 说「好了」;
- 页面变化的等待与去重:重复点击「看起来没反应」的按钮前,先确认是仍在加载还是真的失败;给动作加幂等检查;
- 会话与登录:单独处理认证态(测试账号/会话复用),别让 Agent 每次都从登录开始;涉及真实账号的敏感操作走人工审批;
- 评测要贴合真实:用 WebArena / Mind2Web / OSWorld 一类基准 了解段位,但照搬其任务设计(快照化页面、沙箱站点)到自己的回归集才是关键。
Computer Use 典型失败模式
| 症状 | 根因 | 对策 |
|---|---|---|
| 点错元素/误操作 | 页面结构变化或视觉误读 | 结构优先 + 高置信动作;失败就重观测 |
| 反复点同一个按钮 | 没等页面稳定/没区分「加载中」 | 等待状态稳定 + 幂等去重 |
| 填错表单导致脏数据 | 输入未校验就提交 | 提交前回读校验;用沙箱/测试环境 |
| 触达真实副作用(下单/发信/删除) | 权限与审批缺失 | 敏感动作白名单 + 人工确认(见下) |
| 被验证码/风控挡住 | 行为像 bot | 测试站与生产站隔离;接受「这类任务不适合自动化」 |
四、风险与管控(双保险原则)
- 环境操作 Agent 的铁律:动作的后果必须能被撤销或隔离:
- 代码:沙箱分支 + git 可回滚;
- 网页/桌面:测试账号、mock 支付、快照重置;真实副作用(支付/外发/删除)一律人工审批;
- 不要信任 Agent 的自报完成,验证必须来自环境(测试跑了没、页面状态对不对);
- 执行环境与 LLM 提示内容隔离:网页内容是不可信输入,可能携带注入——由「网页得来的动作意图」必须被清洗/过滤后才可执行,详见 Agent 安全与治理。
五、踩坑清单
- 全量仓库塞上下文:又贵又迷失;先定位再动手;
- 验证是口头不是执行:「应该没问题」不算完成,相关测试实际跑绿才算;
- 自写测试当验收:测试和实现是同一个 Agent 写的,宽松断言自欺;独立评审测试;
- 自修复无预算:失败重试到 token 耗尽;设重试上限并换策略而不是原样重试;
- 不看错误输出就猜:回填信息必须是真实报错,不是模型脑补的原因;
- 共享环境直接改:脏环境、互相踩;用沙箱/独立分支;
- 改了不查引用:局部改坏全局;改前查引用与调用方;
- 网页 Agent 不等页面稳定:重复点击、误判失败;
- 真实副作用无审批:支付/发信/删除这类动作必须人工确认 + 白名单;
- 不设恢复点:长任务中途挂了从零开始;定期 checkpoint 状态;
- 拿基准榜单当 KPI:自己的业务页面与 repo 才是评测集(见 Agent 评测与可观测)。
关联阅读:通用的循环/记忆/工具设计见 Agent 架构通论;环境的沙箱、权限与间接注入防护见 Agent 安全与治理;任务完成度怎么自动判定、基准怎么借鉴见 Agent 评测与可观测;代码检索与仓库理解借用的检索技术见 RAG 与 Agent。