
大语言模型(LLM)在代码生成领域的能力已从“玩具”迈入“生产可用”阶段。然而,将 AI 编程能力真正融入工程化体系,远不止是调用一次 openai.ChatCompletion.create() 那么简单。我们需要解决任务分解、上下文管理、工具调用、错误恢复、质量门禁等系统工程问题。这正是 Agent 工程化 的核心命题——将 AI Agent 视为一等公民,与 CI/CD、代码仓库、测试框架深度集成,构建一条可观测、可回滚、可迭代的自动化代码生成流水线。
本文将带领读者从零构建一个基于 LangChain 和 Docker 的 AI 编程助手 Agent,它能够接收自然语言需求,自动生成 Python 代码、编写单元测试、执行静态检查,并将合格代码提交到 Git 仓库。所有代码均可实际运行,技术要点涵盖 Prompt 工程、工具设计、ReAct 规划、错误熔断、容器化部署等工程化必备环节。
我们的目标不是写一个“聊天机器人”,而是一个可嵌入研发流程的 Agent Worker。整体架构分为三层:
层级 | 职责 | 技术组件 |
|---|---|---|
交互层 | 接收需求(HTTP/RPC),返回生成结果或 PR 链接 | FastAPI + Pydantic |
逻辑层 | Agent 核心:规划、记忆、工具调度 | LangChain + ChatOpenAI + 自定义 Tool |
执行层 | 沙箱执行代码、静态分析、Git 操作 | Docker(python:3.11-slim)、pytest、flake8、GitPython |
![架构图描述:用户通过 API 提交需求,Agent 经过规划调用多个工具(代码生成、测试生成、代码检查、Git 提交),最终返回 PR 链接]
核心工作流:
generate_code → write_test → run_lint → run_test → commit_and_pr。functions 调用,减少幻觉。我们定义五个核心工具,每个工具都包含名称、描述、输入参数和执行函数。
# tools.py
import subprocess
import tempfile
import os
from pathlib import Path
from typing import Dict, Any, Optional
from git import Repo
import pytest
from flake8.api import legacy as flake8
class ToolResult:
def __init__(self, success: bool, output: str, error: Optional[str] = None):
self.success = success
self.output = output
self.error = error
def generate_code_tool(query: str) -> ToolResult:
"""调用 LLM 生成代码(实际被 Agent 内部调用,此处为工具封装)"""
# 此工具实际由 Agent 的 LLM 直接生成,但为了统一接口,我们仍定义为一个“伪工具”
# 真正的代码生成是在 Agent 的思考步骤中由 LLM 输出,但这里我们保留一个工具供显式调用
pass
def write_test_tool(code: str, test_code: str) -> ToolResult:
"""将测试代码写入 tests/ 目录"""
try:
os.makedirs("tests", exist_ok=True)
with open("tests/test_generated.py", "w") as f:
f.write(test_code)
return ToolResult(True, "Test file written successfully.")
except Exception as e:
return ToolResult(False, "", str(e))
def run_lint_tool(file_path: str) -> ToolResult:
"""使用 flake8 检查代码风格"""
style_guide = flake8.get_style_guide()
report = style_guide.check_files([file_path])
if report.total_errors == 0:
return ToolResult(True, "Lint passed.")
else:
errors = [f"{err}" for err in report._deferred_print]
return ToolResult(False, "", "\n".join(errors))
def run_test_tool() -> ToolResult:
"""运行 pytest,返回结果"""
result = subprocess.run(["pytest", "-v", "--tb=short"], capture_output=True, text=True)
if result.returncode == 0:
return ToolResult(True, result.stdout)
else:
return ToolResult(False, result.stdout, result.stderr)
def commit_and_pr_tool(branch_name: str, commit_msg: str, repo_path: str = ".") -> ToolResult:
"""提交代码并创建 PR(需配置 GitLab/ GitHub token)"""
try:
repo = Repo(repo_path)
# 切换或创建新分支
if branch_name not in repo.branches:
repo.create_head(branch_name)
repo.head.reference = repo.branches[branch_name]
repo.head.reset(index=True, working_tree=True)
# 添加所有变更
repo.git.add(A=True)
repo.index.commit(commit_msg)
# push 到远程
origin = repo.remote(name="origin")
origin.push(refspec=f"{branch_name}:{branch_name}", set_upstream=True)
# 创建 PR(调用 GitHub API)
# 此处省略具体 API 调用,使用 PyGithub 或 requests
return ToolResult(True, f"PR created from branch {branch_name}")
except Exception as e:
return ToolResult(False, "", str(e))# agent.py
from langchain.tools import StructuredTool
from langchain.pydantic_v1 import BaseModel, Field
from typing import Type
class CodeGenInput(BaseModel):
requirement: str = Field(description="用户需求描述")
class TestGenInput(BaseModel):
code: str = Field(description="待测代码")
test_code: str = Field(description="测试代码")
# 定义工具实例
tools = [
StructuredTool.from_function(
func=lambda req: generate_code_tool(req), # 实际我们会用 LLM 直接生成,这里作为占位
name="generate_code",
description="根据需求生成 Python 代码",
args_schema=CodeGenInput,
),
StructuredTool.from_function(
func=write_test_tool,
name="write_test",
description="将测试代码写入 tests/test_generated.py",
args_schema=TestGenInput,
),
StructuredTool.from_function(
func=run_lint_tool,
name="run_lint",
description="对指定文件运行 flake8 检查",
args_schema=FileInput, # 需定义
),
StructuredTool.from_function(
func=run_test_tool,
name="run_test",
description="运行 pytest 测试",
args_schema=EmptyInput,
),
StructuredTool.from_function(
func=commit_and_pr_tool,
name="commit_and_pr",
description="提交代码并创建 PR",
args_schema=CommitInput, # 需定义
),
]但注意:generate_code 工具实际上不应该由 Agent 的 LLM 去调用,因为 LLM 本身就能生成代码。更合理的做法是让 Agent 使用 write_test, run_lint, run_test, commit_and_pr 作为外部工具,而代码生成由 Agent 的 LLMChain 直接输出。我们调整设计:Agent 使用 ReAct 的 agent_scratchpad 输出代码,然后调用工具验证。
LangChain 的 create_openai_tools_agent 支持函数调用,非常适合工具型 Agent。
from langchain.agents import create_openai_tools_agent, AgentExecutor
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
llm = ChatOpenAI(model="gpt-4", temperature=0.2)
prompt = ChatPromptTemplate.from_messages([
("system", """你是一位资深的 Python 工程师,负责根据用户需求生成高质量的代码。
你需要按以下步骤操作:
1. 先编写实现代码,保存为 main.py
2. 再编写对应的 pytest 测试代码,保存为 tests/test_main.py
3. 然后运行 flake8 检查 main.py,若有错误则修复
4. 最后运行 pytest,确保全部通过
5. 所有检查通过后,提交代码并创建 PR
你的所有输出必须包含代码块(```python ... ```)或者调用工具。
"""),
MessagesPlaceholder(variable_name="chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
# 我们只需要提供三个工具:run_lint, run_test, commit_and_pr,以及一个自定义的“写文件”工具
# 但为了简化,我们将 write_test 和 write_code 合并为文件写入工具
def write_file_tool(filepath: str, content: str) -> str:
try:
Path(filepath).parent.mkdir(parents=True, exist_ok=True)
with open(filepath, "w") as f:
f.write(content)
return f"File {filepath} written successfully."
except Exception as e:
return f"Error: {str(e)}"
write_file = StructuredTool.from_function(
func=write_file_tool,
name="write_file",
description="将内容写入指定文件",
)
# 重新定义工具列表
tools = [write_file, run_lint_tool_wrapped, run_test_tool_wrapped, commit_and_pr_tool_wrapped]
agent = create_openai_tools_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True, max_iterations=10)使用 FastAPI 提供 HTTP 接口:
# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import uvicorn
app = FastAPI()
class TaskRequest(BaseModel):
requirement: str
repo_url: str # 可选,指定仓库
class TaskResponse(BaseModel):
status: str
pr_url: str | None = None
logs: str
@app.post("/generate")
async def generate_code(req: TaskRequest):
# 将需求注入,并设置工作目录(需克隆 repo)
try:
# 实际需先 git clone 到临时目录
result = executor.invoke({"input": req.requirement})
# 解析结果,提取 PR 链接(从工具输出)
logs = result["output"]
# 简化返回
return TaskResponse(status="success", logs=logs)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)Agent 生成的代码可能包含 os.system 或文件删除操作,必须在 Docker 容器中运行。我们使用 docker-py 在每次任务启动时创建容器,挂载临时卷,执行完毕后销毁。
import docker
client = docker.from_env()
def run_in_sandbox(code: str, test_code: str) -> dict:
container = client.containers.run(
"python:3.11-slim",
command=["bash", "-c", "echo '{}' > main.py && echo '{}' > test_main.py && pytest".format(code, test_code)],
detach=True,
remove=True,
mem_limit="512m",
network_disabled=True, # 禁止网络访问
)
logs = container.logs(stdout=True, stderr=True)
return {"logs": logs.decode()}Agent 的迭代次数有限,但可能陷入循环。我们设置 max_iterations=8,并在工具中增加异常捕获。对于 lint 或 test 失败,Agent 应根据错误信息重新生成代码,我们通过 Prompt 强调“如果检查失败,请分析错误并修正”。
集成 LangChain 的 CallbackManager,将每一步的思考和工具调用记录到结构化日志,同时上报到 ELK 或 Prometheus。
from langchain.callbacks import FileCallbackHandler
handler = FileCallbackHandler("agent.log")
executor = AgentExecutor(..., callbacks=[handler])在 commit_and_pr 之前,强制要求测试覆盖率达到 80% 以上(可通过 pytest-cov 检查)。Agent 可调用 run_cov_tool,若未达标则重新补充测试。
我们模拟一个需求:“实现一个函数 fib(n) 返回第 n 个斐波那契数,使用缓存装饰器,并编写单元测试。”
Agent 的输出(经过简化)如下:
Step 1 思考:需要编写 fib 函数,使用 functools.lru_cache。
Action: write_file,参数 filepath="main.py", content="from functools import lru_cache\n\n@lru_cache(maxsize=None)\ndef fib(n):\n if n < 2: return n\n return fib(n-1) + fib(n-2)"。
Observation: File written.
Step 2 思考:编写测试,测试边界值 n=0,1,10。
Action: write_file,参数 filepath="tests/test_main.py", content="import pytest\nfrom main import fib\n\ndef test_fib_zero():\n assert fib(0) == 0\n\ndef test_fib_one():\n assert fib(1) == 1\n\ndef test_fib_ten():\n assert fib(10) == 55"。
Observation: File written.
Step 3 思考:运行 lint 检查。
Action: run_lint,参数 filepath="main.py"。
Observation: Lint passed.
Step 4:运行 pytest。
Action: run_test。
Observation: Tests passed (3 passed).
Step 5:提交并创建 PR。
Action: commit_and_pr,参数 branch_name="feature/fib-cached", commit_msg="Add cached fib function with tests"。
Observation: PR created: https://github.com/your-repo/pull/123
总耗时约 12 秒(含 API 调用),完全自动化。
ChatPromptTemplate 的 partial 变量,减少 token 消耗。本文展示了如何将 AI 编程能力封装为一个可工程化的 Agent,实现了从需求到 PR 的端到端自动化。关键收获:
未来方向:
AI 编程不是替代开发者,而是将重复劳动自动化,让工程师专注于架构和创新。Agent 工程化正是这座桥梁。欢迎读者在评论区交流落地实践。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。