Skip to content

Code Agent 与计算机使用

产出最高的 Agent,往往是那些能操作真实环境的:改代码库、跑测试、点网页、操作桌面。它们有天然的验证器(测试能跑、页面状态可查)、可回滚、可审计,因此最早进入生产;但也因为「真的执行、真的触达外部系统」而成为风险最高的类别。本页讲两类主流形态——Code Agent(仓库内写码)与计算机使用(Computer Use)(浏览器/桌面操作)——的共同架构、关键工程、沙箱与失败模式;通用控制循环见 Agent 架构通论,安全红线展开见 Agent 安全与治理

一、为什么它们是 Agent 的主战场

特征Code AgentComputer Use(浏览器/桌面)
任务示例修 issue、加功能、代码审查、重构填表、查资料、管理后台、桌面操作
环境验证测试套件 / 编译 / linter页面状态 / 元素断言 / 截图对比
可回滚性git + 沙箱分支,强快照回滚较弱,副作用多(下单/发信)
主要风险改坏别处、装恶意依赖误操作真实服务、注入逃逸
  • 两者共享同一条主干:感知环境 → 决定动作 → 执行 → 用环境反馈修正,区别只在「动作空间与验证方式」;
  • 术语现状提示:此领域模型与工具演进极快,本文讲不随版本变的工程方法;具体模型能力/官方工具的现状请查各自官方文档与最新基准。

二、Code Agent:仓库内的写码循环

工作流全景

读任务(issue/需求) → 理解仓库(结构/符号/相关文件) → 定位修改点
→ 小步编辑(带 diff) → 跑相关测试 → 看失败原因回填 → 自修复(有预算!)
→ 全量验证 → 交 patch/提 PR(附测试证据)

关键工程点

  1. 仓库理解 ≠ 把整个 repo 塞上下文:
    • 第一遍先看:文件树、README、入口/测试目录 → 由 检索 定位相关文件与符号;
    • 维护「repo map」级别信息(模块职责、构建命令、测试命令)存外部,别每轮重读;
  2. 编辑要小步、可检查:每次改动控制在能 review 的规模;改后先跑相关测试,再决定是否扩大;
  3. 验证循环是灵魂:
    • 按改动相关性选测试(全量测试很贵);flaky 测试单独标记,别让偶发失败触发无意义自修复;
    • 自修复要有预算:同一失败最多重试 2-3 次,每次用真实的错误输出回填,而不是猜;
    • 「测试全绿」≠「任务完成」:测试可能是 Agent 自己写的宽松断言——要独立评审测试质量、看覆盖率,或让另一个 agent/人 review;
  4. 改完交差前必须:跑你改动相关的测试 + lint/编译;留下「我改了哪些文件、为什么、验证了什么」的摘要(可读性对 review 与人机协作至关重要);
  5. 环境隔离:在独立分支/沙箱里改与跑,别直接动共享环境;依赖变更(如 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——复用 记忆与状态 的状态结构管理「当前页面任务进度」。

关键工程点

  1. 验证在页面内完成:任务的完成判断要可程序化——元素出现、提交成功页、URL 变化,而不是 Agent 说「好了」;
  2. 页面变化的等待与去重:重复点击「看起来没反应」的按钮前,先确认是仍在加载还是真的失败;给动作加幂等检查;
  3. 会话与登录:单独处理认证态(测试账号/会话复用),别让 Agent 每次都从登录开始;涉及真实账号的敏感操作走人工审批;
  4. 评测要贴合真实:用 WebArena / Mind2Web / OSWorld 一类基准 了解段位,但照搬其任务设计(快照化页面、沙箱站点)到自己的回归集才是关键。

Computer Use 典型失败模式

症状根因对策
点错元素/误操作页面结构变化或视觉误读结构优先 + 高置信动作;失败就重观测
反复点同一个按钮没等页面稳定/没区分「加载中」等待状态稳定 + 幂等去重
填错表单导致脏数据输入未校验就提交提交前回读校验;用沙箱/测试环境
触达真实副作用(下单/发信/删除)权限与审批缺失敏感动作白名单 + 人工确认(见下)
被验证码/风控挡住行为像 bot测试站与生产站隔离;接受「这类任务不适合自动化」

四、风险与管控(双保险原则)

  • 环境操作 Agent 的铁律:动作的后果必须能被撤销或隔离:
    • 代码:沙箱分支 + git 可回滚;
    • 网页/桌面:测试账号、mock 支付、快照重置;真实副作用(支付/外发/删除)一律人工审批;
  • 不要信任 Agent 的自报完成,验证必须来自环境(测试跑了没、页面状态对不对);
  • 执行环境与 LLM 提示内容隔离:网页内容是不可信输入,可能携带注入——由「网页得来的动作意图」必须被清洗/过滤后才可执行,详见 Agent 安全与治理

五、踩坑清单

  1. 全量仓库塞上下文:又贵又迷失;先定位再动手;
  2. 验证是口头不是执行:「应该没问题」不算完成,相关测试实际跑绿才算;
  3. 自写测试当验收:测试和实现是同一个 Agent 写的,宽松断言自欺;独立评审测试;
  4. 自修复无预算:失败重试到 token 耗尽;设重试上限并换策略而不是原样重试;
  5. 不看错误输出就猜:回填信息必须是真实报错,不是模型脑补的原因;
  6. 共享环境直接改:脏环境、互相踩;用沙箱/独立分支;
  7. 改了不查引用:局部改坏全局;改前查引用与调用方;
  8. 网页 Agent 不等页面稳定:重复点击、误判失败;
  9. 真实副作用无审批:支付/发信/删除这类动作必须人工确认 + 白名单;
  10. 不设恢复点:长任务中途挂了从零开始;定期 checkpoint 状态;
  11. 拿基准榜单当 KPI:自己的业务页面与 repo 才是评测集(见 Agent 评测与可观测)。

关联阅读:通用的循环/记忆/工具设计见 Agent 架构通论;环境的沙箱、权限与间接注入防护见 Agent 安全与治理;任务完成度怎么自动判定、基准怎么借鉴见 Agent 评测与可观测;代码检索与仓库理解借用的检索技术见 RAG 与 Agent

基于 VitePress 构建 · 内容以知识共享方式沉淀