你的代码 80% 可以由 AI 写——手把手教你搭建 Coding Agent
一、你的代码,真的必须全部手写吗?
2026 年 5 月,YC 孵化的 Runtime 拿到了新一轮融资——他们做的事情很简单:让团队里每个人都有一个能用自然语言写代码的 AI Agent。HuggingFace 上关于 Coding Agent 的论文也密集涌现——SaaSBench 测试 Agent 在企业级 SaaS 开发中的表现,SpecBench 专门检测 Agent 的"奖励欺骗"行为。
这些信号指向同一件事:Coding Agent 正在从实验室走向工程实践。
但市面上的 Coding Agent 产品(Copilot、Cursor、Devin)要么闭源,要么贵得离谱。本文带你用200 行 Python从零搭建一个能写代码、能调试、能读文件的 AI Coding Agent。不需要任何框架,只需要一个 LLM API Key。
先看最终效果:Agent 收到"在当前目录创建一个 Flask REST API,提供用户 CRUD 接口"的指令后,会自动规划文件结构、生成代码、甚至补上单元测试。
二、Coding Agent 的核心架构
一个可工作的 Coding Agent 由四个模块组成:
| 模块 | 职责 | 关键技术 |
|---|---|---|
| LLM 引擎 | 理解指令、生成代码、决策 | GPT/Claude/DeepSeek API |
| Tool Use 层 | 读写文件、执行命令、搜索 | Function Calling |
| 上下文管理 | 记忆历史、截断策略、文件索引 | Token 窗口管理 |
| Agent 循环 | 观察→思考→行动→观察 | ReAct 范式 |
核心逻辑可以用 15 行伪代码表达:
class CodingAgent: def __init__(self, llm, tools): self.llm = llm # LLM 客户端 self.tools = tools # 工具集合 {name: func} self.messages = [] # 对话历史 def run(self, task: str) -> str: self.messages.append({"role": "user", "content": task}) while True: response = self.llm.chat(self.messages, self.tools) if response.is_final: # LLM 决定结束 return response.content result = self.tools[response.tool_name](response.tool_args) self.messages.append(response.as_tool_result(result))这就是 Agent 的骨架。下面我们逐一实现每个模块。
三、第一步:LLM 客户端封装
先从最基础的 LLM 调用开始。我们使用 OpenAI 兼容接口,方便替换任何模型。
import json import httpx from typing import Optional from dataclasses import dataclass, field @dataclass class ToolCall: """LLM 返回的工具调用请求""" tool_name: str tool_args: dict call_id: str @dataclass class LLMResponse: """LLM 响应封装""" content: Optional[str] = None # 纯文本回复 tool_calls: list[ToolCall] = field(default_factory=list) @property def is_final(self) -> bool: """没有工具调用 = LLM 认为任务完成""" return len(self.tool_calls) == 0 class LLMClient: """OpenAI 兼容的 LLM 客户端""" def __init__(self, api_key: str, base_url: str = "https://api.deepseek.com/v1", model: str = "deepseek-chat"): self.api_key = api_key self.base_url = base_url self.model = model self.client = httpx.Client(timeout=120) def chat(self, messages: list, tools: list[dict]) -> LLMResponse: """发送消息,返回 LLMResponse""" payload = { "model": self.model, "messages": messages, "tools": tools, "temperature": 0.3, # 代码任务降低温度 } resp = self.client.post( f"{self.base_url}/chat/completions", headers={ "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json" }, json=payload ) data = resp.json() choice = data["choices"][0]["message"] response = LLMResponse() response.content = choice.get("content", "") for tc in choice.get("tool_calls", []): func = tc["function"] response.tool_calls.append(ToolCall( tool_name=func["name"], tool_args=json.loads(func["arguments"]), call_id=tc["id"] )) return response这里有几个工程细节值得注意。temperature=0.3是因为代码生成任务需要确定性,太高的随机性会导致语法错误。工具调用使用了 OpenAI 标准的tools参数格式,这意味着你可以无缝切换到 GPT-4、Claude 或任何兼容模型。
四、第二步:Tool Use——给 Agent 装上手脚
Tool Use 是 Coding Agent 区别于普通 Chatbot 的关键。普通 Chatbot 只能"说"代码,Agent 能"写"到文件、能"跑"命令、能"读"报错。
先定义工具的数据结构,再实现具体工具:
import subprocess import os from pathlib import Path # ──── 工具定义(OpenAI Function Calling 格式)──── TOOL_DEFINITIONS = [ { "type": "function", "function": { "name": "read_file", "description": "读取文件内容。用于理解现有代码结构。", "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "相对或绝对路径"} }, "required": ["path"] } } }, { "type": "function", "function": { "name": "write_file", "description": "创建或覆盖文件。传入文件路径和完整内容。", "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "文件路径"}, "content": {"type": "string", "description": "文件完整内容"} }, "required": ["path", "content"] } } }, { "type": "function", "function": { "name": "run_command", "description": "执行 Shell 命令并返回 stdout/stderr。用此工具运行测试、安装依赖、检查语法。", "parameters": { "type": "object", "properties": { "command": {"type": "string", "description": "要执行的命令"}, "cwd": {"type": "string", "description": "工作目录,默认为当前目录"} }, "required": ["command"] } } }, { "type": "function", "function": { "name": "list_files", "description": "列出目录中的文件和子目录。", "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "目录路径,默认当前目录"} }, "required": [] } } } ] # ──── 工具实现 ──── class FileTools: """文件系统操作工具集""" @staticmethod def read_file(path: str) -> str: p = Path(path) if not p.exists(): return f"[Error] 文件不存在: {path}" content = p.read_text(encoding="utf-8") # 超长文件截断,避免撑爆 token 窗口 if len(content) > 8000: content = content[:8000] + "\n... (文件过长,已截断)" return content @staticmethod def write_file(path: str, content: str) -> str: p = Path(path) p.parent.mkdir(parents=True, exist_ok=True) p.write_text(content, encoding="utf-8") return f"[OK] 已写入: {path} ({len(content)} 字符)" @staticmethod def list_files(path: str = ".") -> str: p = Path(path) if not p.exists(): return f"[Error] 目录不存在: {path}" items = [] for entry in sorted(p.iterdir()): tag = "[DIR]" if entry.is_dir() else "[FILE]" size = entry.stat().st_size if entry.is_file() else 0 items.append(f" {tag} {entry.name} ({size:,} bytes)" if tag == "[FILE]" else f" {tag} {entry.name}/") return "\n".join(items) if items else "(空目录)" @staticmethod def run_command(command: str, cwd: str = ".") -> str: try: result = subprocess.run( command, shell=True, cwd=cwd, capture_output=True, text=True, timeout=30 ) out = result.stdout[:4000] # 防止输出过长 err = result.stderr[:2000] rc = f"exit_code={result.returncode}" return f"{rc}\n[stdout]\n{out}\n[stderr]\n{err}" if err else f"{rc}\n[stdout]\n{out}" except subprocess.TimeoutExpired: return "[Error] 命令超时(30s)"run_command是最强大的工具——Agent 可以用它安装依赖、运行 linter、执行测试,甚至通过报错信息自我修正代码。30 秒超时保护了死循环,而输出截断保护了上下文窗口。
五、第三步:Agent 主循环——ReAct 范式落地
前两步准备好了 LLM 和工具,现在把它们串起来。核心是一个 while 循环:LLM 决定调用哪个工具 → 执行工具 → 结果反馈给 LLM → 继续或结束。
from typing import Callable class CodingAgent: """AI Coding Agent 主类""" SYSTEM_PROMPT = """你是一个 AI 编程助手,能读写文件、执行命令来完成任务。 工作流程: 1. 分析用户需求,确定需要创建/修改哪些文件 2. 使用 read_file 了解现有代码,避免重复造轮子 3. 使用 write_file 创建或修改代码文件 4. 使用 run_command 运行测试、检查语法 5. 如果命令报错,分析错误并修正代码,重新执行 原则: - 先读后写:修改前一定先 read_file - 小步快跑:每次只写一个文件,写完立刻用 run_command 验证 - 遇到错误不要猜,用 run_command 看真实的报错信息 - 代码要有适当的注释和错误处理 """ def __init__(self, llm: LLMClient, working_dir: str = "."): self.llm = llm self.working_dir = working_dir self.tools: dict[str, Callable] = { "read_file": lambda **kw: FileTools.read_file(**kw), "write_file": lambda **kw: FileTools.write_file(**kw), "run_command": lambda **kw: FileTools.run_command(**kw), "list_files": lambda **kw: FileTools.list_files(**kw), } self.messages: list[dict] = [ {"role": "system", "content": self.SYSTEM_PROMPT} ] self.max_steps = 15 # 最多 15 轮,防止无限循环 def run(self, task: str) -> str: """执行编程任务,返回最终结果""" self.messages.append({"role": "user", "content": task}) step = 0 while step < self.max_steps: step += 1 print(f"\n{'='*50}\n Step {step}\n{'='*50}") # ── 调用 LLM ── response = self.llm.chat(self.messages, TOOL_DEFINITIONS) # LLM 决定直接回复 = 任务完成 if response.is_final: self.messages.append({ "role": "assistant", "content": response.content }) return response.content or "任务完成" # ── 执行工具调用 ── for tc in response.tool_calls: print(f" 🔧 {tc.tool_name}({json.dumps(tc.tool_args, ensure_ascii=False)})") try: result = self.tools[tc.tool_name](**tc.tool_args) except Exception as e: result = f"[Exception] {type(e).__name__}: {e}" # 裁剪结果,避免 token 爆炸 preview = result[:300].replace("\n", "\\n") print(f" ✅ 结果: {preview}...") # 把工具调用和结果追加到对话历史 self.messages.append({ "role": "assistant", "content": None, "tool_calls": [{ "id": tc.call_id, "type": "function", "function": { "name": tc.tool_name, "arguments": json.dumps(tc.tool_args, ensure_ascii=False) } }] }) self.messages.append({ "role": "tool", "tool_call_id": tc.call_id, "content": result }) return "[Warning] 达到最大步数限制,任务可能未完成"几个关键设计决策: -max_steps=15是经验值,太少完不成复杂任务,太多容易在修复循环中打转 - 每个工具调用的结果都完整记录到对话历史,LLM 能看到完整的执行链 - 异常捕获在最外层,单个工具失败不会让 Agent 崩溃
六、第四步:跑起来
最后写入口函数,把所有零件组装好:
import os def create_agent(api_key: str = None) -> CodingAgent: """工厂函数:创建配置好的 Coding Agent""" api_key = api_key or os.getenv("DEEPSEEK_API_KEY") if not api_key: raise RuntimeError("请设置 DEEPSEEK_API_KEY 或传入 api_key 参数") llm = LLMClient( api_key=api_key, base_url=os.getenv("LLM_BASE_URL", "https://api.deepseek.com/v1"), model=os.getenv("LLM_MODEL", "deepseek-chat") ) return CodingAgent(llm, working_dir=".") if __name__ == "__main__": agent = create_agent() task = """ 在当前目录下创建一个 Flask REST API 项目,要求: 1. 提供 /users 路由,支持 GET(列表)和 POST(创建) 2. 使用 SQLite 存储,字段 id/name/email/created_at 3. 包含一个简单的测试脚本 test_api.py 4. 创建 requirements.txt """ result = agent.run(task) print(f"\n{'='*60}") print(f" 最终结果:\n{result}")执行效果:Agent 会先list_files了解当前目录,然后依次write_file创建app.py→requirements.txt→test_api.py,每写完一个文件就用run_command验证语法,最后用run_command跑一遍测试确认 API 可用。
七、进阶方向
这个 200 行的 Agent 是一个可扩展的骨架。以下是三个实用的进阶方向:
1. 增加代码搜索工具
Agent 经常需要搜索特定函数或类的用法。给TOOL_DEFINITIONS添加一个grep_code工具:
def grep_code(pattern: str, path: str = ".") -> str: """在代码文件中搜索模式匹配的行""" import re results = [] for f in Path(path).rglob("*.py"): for i, line in enumerate(f.read_text(errors="ignore").splitlines(), 1): if re.search(pattern, line): results.append(f"{f}:{i}: {line.strip()}") return "\n".join(results[:50]) # 最多返回 50 条2. 增加 Git 感知能力
让 Agent 在修改代码前自动创建分支,完成后再提交。这需要在write_file前后加入 git 操作,让每一次修改都可追溯、可回滚。
3. 引入 Diff 编辑模式
当前write_file需要传完整文件内容,大文件会浪费大量 token。改用 unified diff 格式,Agent 只传变更部分,大幅降低 token 消耗。OpenAI 的apply_patch思路值得参考。
八、关键要点回顾
本文从零搭建的 Coding Agent 虽然只有 200 行,但涵盖了生产级 Agent 的核心要素:LLM 调用封装、Tool Use 函数定义、对话历史管理、以及基于 ReAct 范式的执行循环。
实际使用中,这个 Agent 能完成文件创建、代码生成、语法检查、测试运行的全自动化流程。如果你手头有重复性的编码任务(创建项目骨架、生成 CRUD 接口、写单元测试),不妨让 Agent 先跑一轮,你只需要 review 和微调。
完整代码已整理在同一个 Python 文件里,复制保存后设置DEEPSEEK_API_KEY环境变量即可运行。
