本文目录
一句话:你在第 7 篇写的最小 Agent,把"查天气"工具换成"读文件 / 改文件 / 跑命令",再加一条"改完自己验证"的循环,就成了 Claude Code / Cursor 这类 Coding Agent 的雏形。这也是面试官想听的"Agent 比调一次 LLM 强在哪"。
Coding Agent = 最小 Agent + 三类工具 + 自验证
回顾第 7 篇:Agent = LLM + 工具 + 循环。Coding Agent 就是把工具集换成"和代码打交道"的那几样:
| 工具 | 作用 |
|---|---|
read_file | 读代码,搞清现状 |
list_dir / search | 在项目里找相关文件 |
write_file / edit | 改代码 |
run | 跑命令(测试 / 编译 / 起服务) |
再加一条灵魂循环:改完 → 跑测试 → 看报错 → 接着改 → 直到通过。这条"自验证闭环"是它比"让 LLM 吐一段代码"强得多的关键。
把第 7 篇的循环改成 Coding Agent
结构和第 7 篇一模一样,只是工具变了。核心还是那个 while 循环:
import json
import os
import subprocess
import sys
from pathlib import Path
from openai import OpenAI
# 先完成第2篇的SDK和环境变量准备;仅使用专门的练习目录。
client = OpenAI(base_url=os.environ["LLM_BASE_URL"], api_key=os.environ["LLM_API_KEY"], timeout=30.0, max_retries=1)
MODEL = os.environ["LLM_MODEL"]
WORKSPACE = Path("coding-lab").resolve()
WORKSPACE.mkdir(exist_ok=True)
def checked_path(path):
target = (WORKSPACE / path).resolve()
if not target.is_relative_to(WORKSPACE):
raise ValueError("只能访问练习目录")
return target
def read_file(path):
target = checked_path(path)
if target.stat().st_size > 1_000_000:
raise ValueError("文件过大,请先缩小范围")
return target.read_text(encoding="utf-8")
def write_file(path, content):
target = checked_path(path)
if len(content.encode("utf-8")) > 1_000_000:
raise ValueError("写入内容过大")
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
return "ok"
def run(cmd):
allowed = {"pytest": [sys.executable, "-m", "pytest", "-q"]}
if cmd not in allowed:
raise ValueError("只允许预先登记的pytest命令")
result = subprocess.run(allowed[cmd], cwd=WORKSPACE, timeout=30, shell=False,
capture_output=True, text=True,
env={"PATH": os.environ.get("PATH", ""), "PYTHONIOENCODING": "utf-8"})
return json.dumps({"returncode": result.returncode, "stdout": result.stdout[-2000:],
"stderr": result.stderr[-2000:]}, ensure_ascii=False)
TOOLS = {"read_file": read_file, "write_file": write_file, "run": run}
def tool_schema(name, description, names):
return {"type": "function", "function": {"name": name, "description": description,
"parameters": {"type": "object", "properties": {key: {"type": "string"} for key in names},
"required": names, "additionalProperties": False}}}
tools_schema = [
tool_schema("read_file", "读取练习目录内的UTF-8文件", ["path"]),
tool_schema("write_file", "写入练习目录内的UTF-8文件", ["path", "content"]),
tool_schema("run", "运行登记的pytest命令,cmd只能是pytest", ["cmd"]),
]
def coding_agent(task, max_steps=15):
messages = [
{"role": "system", "content": "你是工程师。读代码→改代码→跑测试验证,通过了再结束。"},
{"role": "user", "content": task},
]
for _ in range(max_steps): # ← 一定要有步数上限
msg = client.chat.completions.create(model=MODEL, messages=messages, tools=tools_schema).choices[0].message
messages.append(msg.model_dump(exclude_none=True))
if not msg.tool_calls:
return msg.content # 模型说做完了
for call in msg.tool_calls:
try:
args = json.loads(call.function.arguments)
result = TOOLS[call.function.name](**args) # 读/写/跑
except Exception as e:
result = f"工具出错: {e}" # 错误也喂回去,让它自己修
messages.append({"role": "tool", "tool_call_id": call.id, "content": str(result)[:4000]})
return "达到步数上限"
# 在coding-lab中准备utils.py和对应测试,并安装pytest,再运行:
# print(coding_agent("修复 utils.py 里 add() 的 bug,并运行登记的pytest命令"))希望观察到的轨迹是:读文件 → 修改 → 调用登记的 pytest → 查看返回码、标准输出与错误输出 → 再修复。模型也可能过早结束,最终仍要核对真实测试结果。路径和命令白名单只是这个练习的基本约束,不是完整 OS 沙箱;测试本身会执行代码,真实系统还要隔离文件、网络、凭据和副作用。
面试官想听的"它多了什么"
把这几点讲清,这题就满分:
- 会用工具:能读写文件、跑命令,把"想法"落成"对代码的真实操作"。
- 有自验证闭环:靠测试 / 运行结果收口,而不是"看起来对就完事"。
- 能多步规划:复杂任务拆成"读 → 改 → 验"的多步,而非一次性吐答案。
- 能从失败恢复:工具报错喂回去,它自己换路、重试。
常见坑(也是工程难点)
- 不设步数上限:可能反复改不收敛、烧钱。必须
max_steps兜底。 - 放任执行危险命令:
run能跑任意命令——真实系统必须沙箱 + 权限 + 危险操作确认。 - 上下文爆炸:读的文件、跑的输出全堆进上下文,长任务很快撑爆 → 要截断 / 摘要(回到第 5、6 篇)。
- 工具结果不截断:一个命令吐几万行日志直接塞回去 → 上下文炸。结果要截断 / 提炼。
关联面试题
- 「Agent 比直接调用 LLM 多了什么?」(用这篇的四点答)
- 「ReAct 框架的工作流程?和 Plan-and-Execute 的区别?」
- 「Agent 的工具调用失败了怎么处理?」
👉 去 题库 搜 Agent / ReAct / 工具调用。
训练营延伸
从这个单 Coding Agent 再往上——多 Agent 协作、统一工具网关 + 多级沙箱、长程记忆压缩、用强化学习训练 Agent 的决策策略、可观测与私有化上线——就是训练营的集群 Agent 那条完整的线。学完这个模块,你已经站在了那条线的起点。