
AI 编程的争论常常停在"能不能替代程序员",但真问题在工程侧:模型生成的代码如何被约束、被检索、被验证、被回滚。一段"看起来对"的代码如果进不了 CI,就没有价值。本文从上下文构建、约束解码、自验证循环、评测四个环节,给出可落地的实现思路与代码。
模型答不好,多半是上下文给错了。把整个仓库塞进 prompt 既超窗又稀释信号。正确做法是"检索—排序—裁剪":按符号名与调用关系建索引,取与当前编辑点最近的 K 个片段。
# rag/context_builder.py
import ast, re
from dataclasses import dataclass
@dataclass
class Chunk:
path: str
symbol: str
code: str
score: float = 0.0
def index_repo(root):
chunks = []
for p in Path(root).rglob("*.py"):
tree = ast.parse(p.read_text(encoding="utf-8"))
lines = p.read_text().splitlines()
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
code = "\n".join(lines[node.lineno - 1: node.end_lineno])
chunks.append(Chunk(str(p), node.name, code))
return chunks
def build_context(chunks, query, k=8):
q = set(re.findall(r"\w+", query.lower()))
for c in chunks:
# 符号名精确命中权重最高,其次词重叠
name_hit = 1.0 if c.symbol.lower() in query.lower() else 0.0
overlap = len(q & set(re.findall(r"\w+", c.code.lower()))) / (len(q) or 1)
c.score = name_hit * 2 + overlap
top = sorted(chunks, key=lambda c: c.score, reverse=True)[:k]
# 按字符预算裁剪,防止撑爆上下文窗口
budget, out = 6000, []
for c in top:
if budget - len(c.code) < 0: break
out.append(f"# {c.path}::{c.symbol}\n{c.code}")
budget -= len(c.code)
return "\n\n".join(out)检索的关键不是向量库本身,而是符号级切分:按函数/类切块,让召回单元与"要改的东西"对齐,比整文件向量化精准得多。
自由文本输出后靠正则抠 JSON,是脆弱工程。更稳的是约束解码:把模型每步的 token 候选限制在语法允许的集合内。无需自己实现,可用 GBNF / JSON Schema 语法。
# 用 grammar 约束输出为严格的补丁对象
GRAMMAR = r'''
root ::= object
object ::= "{" pair ("," pair)* "}"
pair ::= string ":" value
value ::= string | number | object | array | "true" | "false" | "null"
string ::= "\"" [^"]* "\""
number ::= "-"? [0-9]+ ("." [0-9]+)?
array ::= "[" (value ("," value)*)? "]"
'''
def request_patch(llm, prompt):
raw = llm.generate(
prompt,
grammar=GRAMMAR,
max_tokens=2048,
temperature=0.1, # 代码任务压低随机性
)
return json.loads(raw) # 语法保证下,解析几乎不会失败输出结构固定为 {"file": ..., "old": ..., "new": ...} 的补丁列表,后续就能自动化应用、测试、回滚,而不是人工复制粘贴。
真正把 AI 编程从"玩具"拉到"工具"的,是闭环验证。核心是:跑测试,把失败信息喂回去,最多迭代 N 轮;任何一轮不通过就不合入。
# agent/verify_loop.py
def run_patch(patch, repo):
for edit in patch["edits"]:
path = repo / edit["file"]
src = path.read_text(encoding="utf-8")
if edit["old"] not in src:
return False, f"anchor not found in {edit['file']}"
path.write_text(src.replace(edit["old"], edit["new"], 1), encoding="utf-8")
return True, ""
def verify_loop(llm, task, repo, max_rounds=3):
context = build_context(index_repo(repo), task)
history = []
for r in range(max_rounds):
prompt = f"任务:{task}\n上下文:\n{context}\n历史:\n{history}\n输出补丁 JSON。"
patch = request_patch(llm, prompt)
ok, err = run_patch(patch, repo)
if not ok:
history.append(f"应用失败:{err}")
continue
test = subprocess.run(["pytest", "-q"], cwd=repo,
capture_output=True, text=True)
if test.returncode == 0:
return {"status": "pass", "rounds": r + 1, "patch": patch}
history.append("测试失败:\n" + test.stdout[-2000:]) # 只回传尾部
git_checkout(repo) # 失败即回滚
return {"status": "fail", "history": history}三处细节决定成败:失败即回滚(git checkout)、只回传日志尾部(防止上下文膨胀)、锚点存在性校验(old 找不到就拒应用,避免模型幻觉补丁污染代码)。
凭感觉判断"AI 变强了"不可靠。需要固定任务集、固定环境、可重复指标。
# eval/bench.py
def evaluate(agent, cases):
res = {"pass@1": 0, "avg_rounds": 0, "regression": 0}
for c in cases:
repo = clone(c["repo"], c["commit"])
out = agent.verify_loop(c["task"], repo)
res["pass@1"] += out["status"] == "pass"
res["avg_rounds"] += out.get("rounds", 0)
# 回归:原有测试是否被破坏
res["regression"] += subprocess.run(
["pytest", c["legacy"]], cwd=repo).returncode != 0
n = len(cases)
return {k: v / n for k, v in res.items()}pass@1 看一次通过率,regression 看有没有为修 bug 制造新 bug——后者在生产环境比前者更要命。
AI 编程的有效性不取决于模型多大,而取决于把不确定性关进确定性的笼子里:检索让上下文相关,约束解码让输出可解析,验证循环让错误可回滚,评测让改进可度量。这四件事做扎实,AI 才是一个能进 CI、能上生产的协作对象;缺了任何一环,它就只是会写代码的聊天框。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。