教学目标

看懂一个最小 SWE Agent 是怎么跑起来的

这不是生产级框架,而是一台透明的教学机器。你要掌握的是逻辑: 模型负责判断下一步,程序负责执行动作,history 负责把结果带回下一轮。

Human 提出任务

“列出目录并总结”

Prompt 拼请求

规则 + 历史 + 工具

LLM 决定动作

要不要调用工具

Tool 本地执行

读文件 / 写文件 / 跑命令

History 结果回填

让模型看到真实结果

一句话

Agent 就是一个循环:模型给计划,程序执行,结果回填,模型再计划。

当前代码

主文件是 minimal_swe_agent.py, 另有英文和中文 Markdown 讲义作为源材料。

安全边界

所有文件操作锁在 ~/agent-lab/workspace/;shell 默认需要 y/n 确认。

先建立心智模型

你可以把这个系统想成“小项目经理 + 执行员”。模型像项目经理,读任务、判断下一步、 提出动作;Python 程序像执行员,检查动作是否合法,然后真的去读文件、写文件、跑命令。

模型负责决策

  • 理解用户目标
  • 判断是否需要工具
  • 填出工具名和参数
  • 根据结果继续规划

程序负责执行

  • 解析结构化工具调用
  • 检查 workspace 边界
  • 运行本地函数
  • 把结果写回 history

一轮完整运行

  1. 用户输入任务

    run_agent() 把任务放进 history。

  2. 拼 prompt

    build_prompt() 合并 system prompt、history、tools。

  3. 调用模型

    call_llm() 根据 provider 发给 OpenAI 或 DeepSeek。

  4. 解析工具调用

    parser 把模型输出变成 ToolCall(name, arguments, call_id)

  5. 执行工具

    execute_tool() 调度 read_file / write_file / run_shell

  6. 结果回填

    工具输出追加到 history,下一轮模型就能看到真实世界反馈。

  7. 判断停止

    如果模型不再调用工具,就打印最终回答并结束。

五件套地图

主循环 loop:agent 的心跳

普通聊天是“问一次、答一次”。Agent 是“问模型、执行工具、把结果给模型、再问模型”。 没有 loop,模型最多只能提出动作,没法看到动作结果,也没法修正下一步。

代码位置 minimal_swe_agent.py · run_agent()
停止条件 没有工具调用 / 达到 max_turns / 用户拒绝 shell 后模型决定停止
for turn in range(1, args.max_turns + 1):
    request = build_prompt(...)
    response = call_llm(...)
    calls = extract_function_calls(response)

    if not calls:
        print(final_answer)
        return

    for call in calls:
        output = execute_tool(call)
        history.append(tool_result)

上下文 context:给失忆模型带工作日志

每次 API 调用,模型本质上只看到你这次传过去的内容。history 就是我们替它维护的工作日志: 用户任务、模型消息、工具调用、工具结果都放进去。上下文太长时,程序优先丢旧工具结果。

history 里有什么为什么需要
用户任务让模型记住目标是什么。
模型输出让下一轮知道自己刚才提出过什么。
工具结果让模型基于真实结果继续判断。
压缩策略防止请求无限变长,保留最近且重要的信息。

prompt 拼接:把“任务现场”打包给模型

build_prompt() 是最值得反复看的函数。它说明 agent 没有玄学: 一次模型请求就是 system prompt、history、tools 三块材料的组合。

SYSTEM_PROMPT身份、规则、工具使用纪律
history到目前为止发生的一切
TOOLS工具名、说明、参数 JSON Schema
python3 minimal_swe_agent.py \
  --provider deepseek \
  --dump-prompt \
  --dry-run \
  "List files and summarize."

output parser:把“我想做”变成结构化工单

模型如果只输出“我想运行 ls”,程序很难可靠执行。Function calling 的价值是让模型输出结构化数据: 工具名是什么,参数是什么,call_id 是什么。

脆弱文本

好的,我来运行 ls 看一下目录。

结构化调用

{
  "name": "run_shell",
  "arguments": {"command": "ls"}
}

executor:真正碰电脑的地方

executor 是信任边界。模型提出动作,executor 决定是否执行、怎么执行、执行范围在哪里。 这个项目故意只给三个工具,够理解核心,不被工程复杂度淹没。

read_file读 workspace 内的 UTF-8 文本文件
write_file写 workspace 内的 UTF-8 文本文件
run_shell在 workspace 内运行命令,默认先问 y/n
硬边界: /Users/karasuakamatsu/agent-lab/workspace 路径解析后必须还在这个目录里,否则拒绝。

OpenAI / DeepSeek:同一内核,两种 API 方言

DeepSeek 可以做这个教学实验。它和 OpenAI 的差别不在 agent 思想,而在请求/响应格式。 代码用 provider boundary 把差异隔离起来。

client.responses.create(
    model="gpt-5.4",
    instructions=SYSTEM_PROMPT,
    input=history,
    tools=TOOLS
)

工具结果回填形态:{"type": "function_call_output", "call_id": "...", "output": "..."}

client.chat.completions.create(
    model="deepseek-v4-flash",
    messages=[system_message] + history,
    tools=chat_tools(),
    tool_choice="auto"
)

工具结果回填形态:{"role": "tool", "tool_call_id": "...", "content": "..."}

本地运行方式

先安装依赖,再选择 provider。不要把 API key 写进文件;用环境变量。

只看 prompt,不调用模型

cd /Users/karasuakamatsu/agent-lab
python3 minimal_swe_agent.py \
  --provider deepseek \
  --dump-prompt \
  --dry-run \
  "List files."

真实调用 DeepSeek

python3 -m pip install --upgrade -r requirements.txt
read -s DEEPSEEK_API_KEY
export DEEPSEEK_API_KEY
python3 minimal_swe_agent.py \
  --provider deepseek \
  "List the current directory files and summarize them."

推荐读代码路线

不要从第一行硬啃。按下面顺序读,脑子里会先有地图,再看局部细节。

  1. SYSTEM_PROMPT先看模型被要求遵守什么规则。
  2. TOOLS再看模型可以调用哪些动作。
  3. run_agent()看主循环如何把模型和工具串起来。
  4. build_prompt()看一次请求如何被组装出来。
  5. call_llm()看 OpenAI / DeepSeek 差异被隔离在哪里。
  6. parser看模型输出如何变成工具调用。
  7. executor看真实动作和安全边界在哪里。