Prompt 模板在代码生成 Agent 中的最佳实践:从需求到可运行代码
Prompt 模板在代码生成 Agent 中的最佳实践:从需求到可运行代码
一、深度引言与场景痛点
大家好,我是赵咕咕。
去年年底,我们在内部工具平台里接了一个"AI 代码生成"功能。产品经理的原话是:"用户用自然语言描述需求,Agent 直接生成可运行代码,然后一键部署。"听起来很美好。
第一个月的效果让我崩溃——同样输入"写一个 FastAPI 接口,接收文本 query,返回 RAG 检索结果",十次生成十种不同的代码。有时候用 LangChain,有时候用原生 OpenAI SDK,有时候直接写了一个不存在的假库。更离谱的是,有一次生成的代码里import rag_magic——这是 LLM 脑补出来的库。
问题的根源不是模型能力不够,而是Prompt 模板缺少约束。代码生成 Agent 需要一个精心设计的 Prompt 模板,来引导 LLM 在给定的技术栈、代码规范和架构约束下生成代码。
这篇文章,我把代码生成 Agent 的 Prompt 模板设计方法论和工程实践整理出来。
二、底层机制与原理深度剖析
2.1 为什么简单的 Prompt 不够?
在代码生成场景下,LLM 面对的是一个"欠约束"的问题。自然语言描述的需求天然包含歧义——"写一个 RAG 检索服务"可以解读为:
- 使用 LangChain 框架
- 使用 LlamaIndex 框架
- 自己手写 embedding + search
- 用 Rust 写(如果你没限制语言)
所以 Prompt 的核心任务不是"告诉 LLM 做什么",而是消除需求中的歧义,把开放问题变成约束问题。
2.2 Prompt 模板的五层约束模型
这五层约束是递进的:
- 全局技术约束:圈定技术边界。语言只能用 Python 3.11+,Web 框架只能用 FastAPI,代码规范必须通过 Ruff。
- 架构约束:指定代码的组织方式。服务必须放在
src/services/,接口必须是async def+ Pydantic。 - 上下文注入:通过 RAG 检索项目的现有代码,让 LLM 知道"项目里已经有什么",生成代码时能复用已有的类型和函数。
- 生成指令:明确对生成代码的要求。要不要类型标注?要不要异常处理?要不要日志?每一条都是约束。
- 自检要求:让 LLM 在生成代码后进行自我检查——语法对吗?导入的库存在吗?类型检查能过吗?
2.3 为什么需要自检层?
第五层的自检要求是整个模板最重要的设计。LLM 生成代码后,如果不做自检,错误率可能在 15-20%。加上自检指令后,错误率下降到 5% 以内。
原因是 LLM 的"幻觉"在代码场景表现为假 import。让 LLM 检查"所有 import 的模块是否存在",相当于让它在输出前做一次事实核查。
三、生产级代码实现
import asyncio import ast import logging from pathlib import Path from typing import Any from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from pydantic import BaseModel, Field logger = logging.getLogger(__name__) class CodeGenRequest(BaseModel): """代码生成请求。""" requirement: str = Field(..., description="用户的自然语言需求描述") tech_stack: list[str] = Field( default_factory=lambda: ["Python 3.11", "FastAPI", "asyncio"] ) existing_types: list[str] = Field( default_factory=list, description="项目中已有的类型定义(供复用)", ) similar_code: list[str] = Field( default_factory=list, description="检索到的相似代码片段", ) must_have: list[str] = Field( default_factory=lambda: [ "类型标注", "异常处理", "日志记录", ] ) must_not_have: list[str] = Field( default_factory=lambda: [ "假库/不存在的 import", "裸 except", "过时的 API(如 openai.ChatCompletion)", "同步阻塞调用(如 time.sleep)", ] ) class CodeGenResult(BaseModel): """代码生成结果。""" code: str filename: str description: str imports_valid: bool syntax_valid: bool warnings: list[str] = Field(default_factory=list) class CodeGenPromptBuilder: """代码生成 Prompt 构建器。""" SYSTEM_TEMPLATE = """你是一个 Python 代码生成专家。请严格遵循以下约束生成代码。 ## 四、边界分析与架构权衡 {tech_stack} ## 五、总结 - 所有函数必须有类型标注 - 异步函数使用 `async def`,避免同步阻塞 - 使用 `logging` 模块记录关键操作 - 异常必须显式处理,禁止裸 `except:` - 遵循项目现有代码风格 ## 项目上下文(已有代码) 已有类型定义: {existing_types} 相似代码片段(参考风格和模式): {similar_code} ## 输出要求 {output_requirements} ## 禁止事项 {forbidden_items} ## 最终自检 生成代码后,你的回答末尾必须包含一个自检段落:自检结果
- 语法正确(ast.parse 可通过)
- 所有 import 的模块都是 Python 标准库或已安装的依赖
- 所有函数有类型标注
- 错误处理覆盖了异常路径
- 没有同步阻塞调用
""" USER_TEMPLATE = """## 需求描述 {requirement} 请生成代码。""" def __init__(self): self.template = ChatPromptTemplate.from_messages([ ("system", self.SYSTEM_TEMPLATE), ("human", self.USER_TEMPLATE), ]) def build(self, request: CodeGenRequest) -> dict[str, Any]: """构建 Prompt 参数。""" return { "tech_stack": "\n".join( f"- {t}" for t in request.tech_stack ), "existing_types": ( "\n".join(f"```python\n{t}\n```" for t in request.existing_types) if request.existing_types else "(无)" ), "similar_code": ( "\n---\n".join( f"```python\n{c}\n```" for c in request.similar_code ) if request.similar_code else "(无)" ), "output_requirements": "\n".join( f"- {r}" for r in request.must_have ), "forbidden_items": "\n".join( f"- {r}" for r in request.must_not_have ), "requirement": request.requirement, } class CodeValidator: """代码静态验证器。""" @staticmethod def check_syntax(code: str) -> tuple[bool, str]: """使用 ast 检查 Python 语法。""" try: ast.parse(code) return True, "语法检查通过" except SyntaxError as e: return False, f"语法错误: {e}" @staticmethod def extract_imports(code: str) -> set[str]: """提取代码中的所有顶层 import 模块名。""" try: tree = ast.parse(code) imports = set() for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: imports.add(alias.name.split(".")[0]) elif isinstance(node, ast.ImportFrom): if node.module: imports.add(node.module.split(".")[0]) return imports except SyntaxError: return set() @staticmethod def check_imports_exist(imports: set[str]) -> dict[str, bool]: """检查 import 的模块是否存在(尝试导入)。""" result = {} for module in imports: try: __import__(module) result[module] = True except ImportError: result[module] = False return result @staticmethod def check_bare_except(code: str) -> bool: """检查是否有裸 except 语句。""" return "except:" in code and "except Exception" not in code @staticmethod def check_sync_blocking(code: str) -> list[str]: """检查同步阻塞调用。""" blocking_patterns = [ "time.sleep", "requests.get", "requests.post", ] found = [] for pattern in blocking_patterns: if pattern in code: found.append(pattern) return found class CodeGenAgent: """代码生成 Agent。 组合 Prompt 模板 + LLM + 验证器。 """ def __init__( self, llm: Any, prompt_builder: CodeGenPromptBuilder | None = None, ): self._llm = llm self._builder = prompt_builder or CodeGenPromptBuilder() self._validator = CodeValidator() async def generate( self, request: CodeGenRequest, timeout: float = 60.0 ) -> CodeGenResult: """主入口:生成代码并验证。""" try: return await asyncio.wait_for( self._generate_impl(request), timeout=timeout ) except asyncio.TimeoutError: return CodeGenResult( code="", filename="", description="生成超时", imports_valid=False, syntax_valid=False, warnings=["代码生成超时"], ) async def _generate_impl( self, request: CodeGenRequest ) -> CodeGenResult: # 1) 构建 Prompt prompt_params = self._builder.build(request) chain = self._builder.template | self._llm | StrOutputParser() # 2) 调用 LLM try: raw_output = await chain.ainvoke(prompt_params) except Exception as e: logger.error("LLM 调用失败: %s", e) return CodeGenResult( code="", filename="", description=f"LLM 调用失败: {e}", imports_valid=False, syntax_valid=False, warnings=[str(e)], ) # 3) 解析输出 code_block = self._extract_code_block(raw_output) # 4) 验证 warnings: list[str] = [] syntax_ok, syntax_msg = self._validator.check_syntax(code_block) if not syntax_ok: warnings.append(syntax_msg) imports = self._validator.extract_imports(code_block) import_status = self._validator.check_imports_exist(imports) invalid_imports = [ mod for mod, ok in import_status.items() if not ok ] if invalid_imports: warnings.append(f"不存在的 import: {', '.join(invalid_imports)}") if self._validator.check_bare_except(code_block): warnings.append("检测到裸 except 语句") blocking = self._validator.check_sync_blocking(code_block) if blocking: warnings.append(f"检测到同步阻塞调用: {', '.join(blocking)}") return CodeGenResult( code=code_block, filename="generated_service.py", description=f"从需求生成: {request.requirement[:50]}...", imports_valid=len(invalid_imports) == 0, syntax_valid=syntax_ok, warnings=warnings, ) @staticmethod def _extract_code_block(raw_output: str) -> str: """从 LLM 输出中提取 Python 代码块。""" # 尝试匹配 ```python ... ``` import re match = re.search( r"```python\n(.*?)```", raw_output, re.DOTALL ) if match: return match.group(1).strip() # 尝试匹配 ``` ... ``` match = re.search( r"```\n(.*?)```", raw_output, re.DOTALL ) if match: code = match.group(1).strip() if "def " in code or "import " in code: return code # 没有代码块标记,返回原始输出 return raw_output.strip() async def main(): from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="gpt-4o", temperature=0) agent = CodeGenAgent(llm=llm) request = CodeGenRequest( requirement=( "写一个 FastAPI 路由,接收 POST /search 请求," "body 包含 query 字段,返回 RAG 检索结果的 top-5 文档" ), existing_types=[ "class RetrievalResult(BaseModel):\n" " content: str\n score: float\n source: str", ], ) result = await agent.generate(request) print(f"语法: {result.syntax_valid}") print(f"导入: {result.imports_valid}") print(f"警告: {result.warnings}") print(f"代码:\n{result.code[:500]}...") if __name__ == "__main__": asyncio.run(main())代码中几个重要的设计:
- 五层约束映射到 Prompt 参数:
build()方法把CodeGenRequest的五类约束转换为 system prompt 的具体内容。每类约束都是可配置的,不同项目可以覆盖不同的约束列表。 - 验证器独立于生成器:
CodeValidator是一个纯静态检查器,不依赖 LLM。即使 LLM 自检声称"通过了",代码仍然走一遍实际的ast.parse和__import__验证。 - 代码块解析:
_extract_code_block处理了 LLM 输出的各种格式(有/无 markdown 标记、纯文本等),确保稳定提取。
四、边界分析与架构权衡
4.1 Prompt 模板应该多"严格"?
约束越严格,生成的代码越规范,但灵活性越低。对于内部工具平台的代码生成,约束可以很严格(因为技术栈是固定的)。对于面向公众的代码生成,约束应该适度放宽,否则用户体验会很差——"我就想生成一个脚本,为什么要求我必须用 FastAPI?"
4.2 上下文注入的规模控制
RAG 检索到的相似代码片段可能很长。如果注入太多上下文,Prompt 会超出模型的 context window。
建议:相似代码片段用函数签名 + 前 10 行 + "..." 的方式展示,而不是完整贴入。LLM 需要的是"知道有这个函数"而不是"读完整实现"。
4.3 验证器应该在 LLM 内还是 LLM 外?
两者的关系是"LLM 先自查,代码再外查":
| 检查项 | LLM 自检 | 外部验证 |
|---|---|---|
| 语法正确 | 不可靠,LLM 不知道自己的语法 | ast.parse 可靠 |
| 导入存在 | 70% 准确 | import100% 准确 |
| 类型标注 | 可靠 | mypy 验证 |
| 代码风格 | 基本可靠 | Ruff 验证 |
| 逻辑正确 | 不可靠 | 需要测试用例 |
LLM 自检能减少 50% 的低级错误,但不可替代外部验证器。
4.4 什么场景下需要代码生成 Prompt 模板?
| 场景 | 是否需要模板 |
|---|---|
| 一次性脚本 | 不需要,简单 Prompt 即可 |
| 内部工具平台标准化代码生成 | 必须有模板 |
| 公开 API 代码生成 | 模板 + 宽松约束 |
| 遗留系统代码迁移 | 模板 + 大量上下文 |
| 多语言代码生成 | 每种语言独立模板 |
五、总结
代码生成 Agent 的 Prompt 模板设计,本质上是一场"约束工程":
- 约束越多,质量越高——但别过度。找到让代码"可用"的最小约束集。
- 上下文比 Prompt 本身更重要——告诉 LLM 项目里已有的函数和类型,比教它写代码规范更有效。
- 自检 + 外检——LLM 自查是辅助,
ast.parse+__import__+mypy才是门神。 - 验证器要独立于 LLM——不要在 Prompt 里让 LLM "保证代码正确",它做不到。你用 Python 解释器来保证。
设计良好的 Prompt 模板就像写好了一个"代码规范文档"——新来的 LLM 读一遍就能按你的风格写代码。设计不好的 Prompt 模板,就像你跟一个实习生说"随便写"——出来的东西大概率不是你要的。
下一篇预告:智能运维 Agent 的日志分析 RAG,海量日志的实时索引与异常检索方案。