一、你的代码,真的必须全部手写吗?

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.pyrequirements.txttest_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 环境变量即可运行。

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务

更多推荐