把一份 Markdown 需求翻译成可运行的 Python 代码草案,同时把"哪些地方我没把握"一起交出来。
源码仓库 https://github.com/0x697551b0/hw2-coding-agent,作者刘浩天 / 2412190219。
v1.0.0,Python 3.10+,默认零第三方依赖,测试 48/48。
这是「编码 → 审查 → 修复」工作流的第一个节点。它不做"一句话生成一个系统",输入是一份有
约定格式的 Markdown 需求,输出是一个源码文件加一份 draft_manifest.json(记录生成策略与
开放问题),然后交给下游的审查 Agent 去挑毛病。
引擎有两个。默认是离线模板引擎,确定性、不联网、零依赖;配上 API Key 可以切 LLM 引擎。 离线那套不是凑数用的:评审者 clone 下来就能把整条流水线跑通,另外"模型是不是真的比模板 更懂需求"这个问题,有了它才有个可复现的对照基线。
需求文档(人写)
examples/spec_user_store.md
│
▼
╔══════════════════════════════════════════════╗
║ ① coding-agent ← 本仓库 ║
║ 需求解析 → 生成草案 → 语法校验 → 落盘 ║
║ 产出 user_store.py + draft_manifest.json ║
╚═══════════════════┬══════════════════════════╝
▼ worktree/(代码草案)
┌──────────────────────────────────────────────┐
│ ② review-agent(复用 Homework 1 codeguard) │
│ → 契约化 finding 清单 + 门禁判定 │
└───────────────────┬──────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ ③ fix-agent → 按 finding_id 自动修复 │
└───────────────────┬──────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ ④ workflow 编排器 → 复验(回到②)、报告 │
└──────────────────────────────────────────────┘
上游是需求文档,下游是审查 Agent。这个 Agent 不判断代码好坏,那是下游的事。它的职责边界 就是:保证交出去的东西语法正确、路径安全,把不确定的地方写清楚。
需要 Python 3.10+(我这边用的是 3.14.7)。默认不装任何第三方包,只有 engine=llm 才要 httpx。
cd homework2/coding-agent
pip install -e . # 可选:装成 coding-agent 命令
pip install -e ".[llm]" # 可选:只有要用真实模型时才需要不装也能跑,直接用 python -m coding_agent。
python -m coding_agent render examples/spec_user_store.md # 打印草案,不写磁盘
python -m coding_agent spec examples/spec_user_store.md # 看需求被解析成什么
python -m coding_agent generate examples/spec_user_store.md --out ./worktree --overwrite想用真模型的话:
$env:CODING_ENGINE = "llm"; $env:CODING_API_KEY = "sk-xxxxxxxx"
python -m coding_agent generate examples/spec_user_store.md --out ./worktree-llm --overwrite
# 换服务商(通义 / OpenAI / 自建网关)只需改 base_url 与 model,代码零改动
python -m coding_agent generate examples/spec_user_store.md --out ./worktree-llm --engine llm --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --model qwen-plusrender 出来的东西长这样(真实输出,中间省略了 find_users 和 load_config):
$ python -m coding_agent render examples/spec_user_store.md
# ===== user_store.py(37 行)=====
"""用户存储服务(首版草案,由编码 Agent 生成)。"""
import hashlib
import json
AUTH_TOKEN = "sk-live-9f8a7b6c5d4e3f2a1b0c"
def create_user(cursor, name, email, age):
assert name and email, "name 与 email 不能为空"
cursor.execute(f"INSERT INTO users (name, email, age) VALUES ('{name}', '{email}', {age})")
return cursor.lastrowid
def get_user(cursor, user_id):
assert user_id, "user_id 不能为空"
try:
cursor.execute("SELECT id, name, email, age FROM users WHERE id = ?", (user_id,))
return cursor.fetchone()
except Exception:
return None
… (find_users / load_config 略)
# ===== 笔记 =====
# [limitation] 当前离线模板刻意还原「首版草案」的常见成熟度问题(拼 SQL、硬编码配置、宽泛异常等),
# 供下游审查 Agent 检出与修复,不应当作生产代码直接使用。
这份草案是故意写成这样的:主流程能跑,安全和可靠性上留着初级开发者常见的坑。它的用处是 给下游审查 Agent 提供真实输入,没有这些坑,「审查 → 修复」就没得演。
同一份需求换个档位就是已合规的版本,46 行:
AUTH_TOKEN = os.environ["USER_STORE_AUTH_TOKEN"] # 密钥从环境变量读
def create_user(cursor, name, email, age):
"""创建用户。"""
if not name or not email: # 显式校验替代 assert
raise ValueError("name 与 email 不能为空")
cursor.execute("INSERT INTO users (name, email, age) VALUES (?, ?, ?)", (name, email, age,))
return cursor.lastrowid解析和生成的结果:
$ python -m coding_agent spec examples/spec_user_store.md
标题:用户存储服务
模块:user_store
数据表:users 字段:id, name, email, age
需求 4 条:
- R1 创建用户 → create_user(cursor, name, email, age)
- R2 按主键查询用户 → get_user(cursor, user_id)
- R3 按名称关键词搜索用户 → find_users(cursor, keyword, limit_collector)
- R4 加载服务配置 → load_config(path)
$ python -m coding_agent generate examples/spec_user_store.md --out ./worktree --overwrite
✅ 按需求生成 1 个文件(user_store.py),共 37 行;1 个待确认问题
输出目录:worktree
[limitation] 当前离线模板刻意还原「首版草案」的常见成熟度问题……
worktree/ 里有两个文件:user_store.py(37 行 / 976 字节 / sha256 ed6b8435…)和
draft_manifest.json(文件清单 + 生成策略 + 开放问题)。
优先级是命令行参数 > 环境变量 > .env > 默认值,前缀 CODING_。
| 子命令 | 作用 | 写磁盘 | 需要 API Key |
|---|---|---|---|
render <spec> |
渲染草案并打印到 stdout | 否(内部用临时目录做校验,用完即删) | 否 |
generate <spec> |
生成草案并写入 --out 目录 |
是 | 否(--engine llm 除外) |
spec <spec> |
只解析需求并打印结构 | 否 | 否 |
info |
版本、环境与当前生效配置 | 否 | 否 |
公共参数:--engine offline|llm、--profile realistic|clean、--api-key、--model、
--base-url、--json、--verbose。generate 额外要 --out(必填),可选 --overwrite。
python -m coding_agent render examples/spec_user_store.md --profile clean # 已合规版本
python -m coding_agent generate examples/spec_user_store.md --out ./worktree --overwrite --json
python -m coding_agent spec examples/spec_user_store.md --json # 需求结构
python -m coding_agent.worker --request-file job.json --response-file out.json # 子进程模式环境变量:
| 变量 | 默认值 | 说明 |
|---|---|---|
CODING_ENGINE |
offline |
offline / llm |
CODING_PROFILE |
realistic |
realistic / clean |
CODING_API_KEY |
— | engine=llm 时必填 |
CODING_BASE_URL |
https://api.deepseek.com |
OpenAI 兼容服务根地址 |
CODING_MODEL |
deepseek-chat |
模型名 |
CODING_TEMPERATURE |
0.2 |
采样温度,生成代码宜低 |
CODING_MAX_TOKENS |
8000 |
单次生成上限,小于 256 会被校验拒绝 |
CODING_TIMEOUT |
120 |
单次请求超时(秒) |
CODING_MAX_RETRIES |
3 |
可重试错误的最大重试次数 |
CODING_MAX_FILE_BYTES |
200000 |
单文件字节上限,超限文件被丢弃并登记为 limitation |
CODING_WORKSPACE_ROOT |
空 | 输出根目录,为空则用进程当前目录 |
CODING_VERBOSE |
0 |
置 1 输出 DEBUG 日志 |
.env 是自写的 31 行解析器读的(KEY=VALUE、# 注释、export 前缀、成对引号),没引
python-dotenv,这样只有标准库也能跑。示例见 .env.example。
退出码只有三种:
| 退出码 | 含义 |
|---|---|
0 |
成功 |
2 |
用法 / 配置 / 运行期失败(需求文件不存在、engine=llm 但没配 Key、模型输出解析不出来) |
1 |
子进程 Worker 专用:响应信封 status=failed |
render/generate/spec/info 失败一律是 2,因为 SpecError、EngineError、LLMError
都是 CodingAgentError 的子类,CLI 用一个分支就兜住了,FileNotFoundError 另有一条分支,
也返回 2。Worker 不一样:它得把失败信息塞进响应信封交给编排器,所以用退出码 1 表示这次
任务失败。
coding-agent/
├── coding_agent/
│ ├── agent.py 346 行 ★ 主流程:解析 → 生成 → 校验 → 落盘 → 报告;协议入口
│ ├── spec.py 285 行 ★ 需求解析(Markdown → SpecModel,偏差降级为 warnings)
│ ├── templates.py 503 行 ★ 离线模板引擎(realistic / clean 两档)
│ ├── workspace.py 137 行 ★ 输出路径沙箱 + 落盘(28 种后缀白名单、体积上限、覆盖策略)
│ ├── prompts.py 145 行 LLM 提示词 + 三层容错 JSON 解析
│ ├── llm.py 175 行 LLM 客户端(httpx + 退避重试 + ScriptedLLM 替身)
│ ├── protocol.py 334 行 Agent 契约信封(四仓库等价副本)
│ ├── models.py 141 行 CodeFile / DraftNote / DraftStrategy / CodingResult
│ ├── config.py 148 行 配置加载(环境变量 / .env / 校验 / 密钥脱敏)
│ ├── worker.py 106 行 子进程 Worker(stdin/stdout 单次 JSON 往返,含 UTF-8 重配)
│ ├── cli.py 213 行 命令行界面
│ ├── errors.py 31 行 异常体系
│ └── __init__.py / __main__.py 对外 API 与入口
├── tests/ 633 行 48 个用例,全部离线
│ ├── test_agent.py 184 行 13 例:端到端、协议入口、LLM 失败路径、worker stdout 回归
│ ├── test_spec.py 81 行 10 例:中英文标题、主键推断、格式容错
│ ├── test_templates.py 89 行 8 例:两档位性质断言、确定性、占位实现
│ ├── test_workspace.py 79 行 8 例:路径越界、保留名、覆盖策略、UTF-8/LF
│ ├── test_protocol.py 100 行 9 例:契约往返、版本校验、未知字段透传
│ └── support.py 99 行 临时目录 / 样例需求 / 断言工具
├── examples/spec_user_store.md 23 行 示例需求
├── run_tests.py 136 行 零依赖测试运行器(不装 pytest 也能跑)
├── pyproject.toml 31 行 dependencies = []
├── .env.example 31 行 环境变量示例
├── .gitignore 55 行 .env / worktree/ / job.json 在忽略列表内
├── LICENSE MIT
├── Design.md ★ 设计文档
└── README.md 本文件
分层大致是这样:
接口层 cli.py(render/generate/spec/info) · worker.py(子进程协议)
▼
编排层 agent.py :: CodingAgent —— 四步流程 + 语法/体积校验 + 落盘 + handle()
▼ ▼
生成层 templates.py(离线纯函数) 边界层 workspace.py(唯一写盘通道)
llm.py + prompts.py protocol.py · models.py
▼
事实层 spec.py(Markdown → IR)
横切:config.py(配置) · errors.py(异常体系)
模块分工和各自的边界:
| 模块 | 负责 | 不负责 |
|---|---|---|
agent.py |
串起四步、校验、落盘、协议入口 | 不实现模板、不发 HTTP、不解析需求 |
spec.py |
Markdown → SpecModel,偏差降级为 warnings |
不判断需求合理性 |
templates.py |
按函数语义(insert/select/update/delete/config/stub)生成函数体 | 不读盘、不写盘 |
workspace.py |
路径校验 + 后缀白名单 + 体积上限 + 覆盖策略 + UTF-8/LF | 不知道代码内容对不对 |
protocol.py |
四个仓库共享的请求/响应信封 | 无业务语义 |
离线引擎我当成一等公民来做,不是"没有 Key 时的降级方案":它默认开启、是纯函数、同输入必 得同输出,因此无 Key 可演示、可回归断言,还能当模型质量的对照基线。
生成完立刻校验,不让问题往下游流。ast.parse 拦语法错误、按字节数拦超限文件;一个 Agent
如果生成完不校验就写下去,错误会跑到审查 Agent 那里,变成"审查帮编码擦屁股"。
所有落盘只走 Workspace 一个通道,..、盘符、Windows 保留名、后缀白名单全拦。写文件是
高权能操作,越权写入是这类项目最容易被忽略的风险面。
不确定就申报。DraftNote 分 decision / question / limitation 三类,连同
draft_manifest.json 一起交出去,让"哪块是猜的"能被下游和人看见,不伪装成确定。
契约用 JSON 信封,不靠 import 类型。四个仓库各持一份等价的 protocol.py,这样每个仓库都能单独
clone、单独跑通,不必靠 pip 依赖互相绑定;代价是改契约要同步四处。
需求格式选 Markdown 加轻量约定,因为需求是人写的,评审要能直接读改。代价是解析是启发式的,
## 约束 会被并进 storage 字段。
默认不覆盖内容不同的同名文件,多一步 --overwrite 换一个明确的扳机。
realistic 档位是故意产出拼 SQL、硬编码密钥、宽泛 except 的,没有这些缺陷就没有"审查→
修复"可演;代价是这份代码不能当生产代码用,笔记里也写明了。LLM 失败不静默回退到模板,
回退是编排器的决策,静默降级会让编排器以为"模型干了活"。
细节都在 Design.md。
请求 AgentRequest 的通用字段:contract_version(当前 "1.0",主版本不符即拒)、agent
(必须 "coding")、task_id、run_id、round、objective、payload(本 Agent 私有)、
shared(只读快照)、metadata(未知键透传)。
payload 里本 Agent 认这些键:
| 键 | 必填 | 说明 |
|---|---|---|
out_dir |
是(或用 workdir) |
代码写到哪个目录 |
spec_path / spec_text |
二选一 | 需求文件路径或内联文本 |
profile |
否 | realistic / clean |
overwrite |
否 | 默认 true,协议路径下默认覆盖 |
extra_constraints |
否 | 追加给 LLM 的额外约束(仅 engine=llm) |
响应字段:status(ok/failed)、summary、payload(结构化结果,不含文件正文,正文
走磁盘)、artifacts(每个源码文件加 draft_manifest.json)、metrics
(elapsed_seconds/files/total_lines/token_usage)、diagnostics
(engine/profile/generation_mode/model/spec_warnings)、error(failed 时非空)。
payload 里有一项值得单说:spec_path。它留档"这份代码是从哪份需求生成的",来源优先取
调用方传入的 spec_path,其次取 SpecModel.source_path,内联 spec_text 时为空。它只出现
在 payload() 里,不写进 draft_manifest.json。
真实往返,请求文件:
{
"contract_version": "1.0", "agent": "coding", "task_id": "coding-r1",
"run_id": "demo", "round": 1, "objective": "按需求生成用户存储模块",
"payload": { "spec_path": "examples/spec_user_store.md",
"out_dir": "./worktree", "overwrite": true }
}python -m coding_agent.worker --request-file job.json --response-file out.json # 退出码 0out.json 的关键片段(真实输出,长字段有省略):
{
"agent": "coding", "status": "ok",
"summary": "按需求生成 1 个文件(user_store.py),共 37 行;1 个待确认问题",
"payload": {
"module": "user_store", "engine": "offline", "profile": "realistic",
"generation_mode": "template", "output_dir": "worktree",
"spec_path": "examples\\spec_user_store.md",
"files": [{ "path": "user_store.py", "lines": 37, "code_lines": 25, "size_bytes": 976,
"sha256": "ed6b8435528bf140cd72431cad86c3806903d7f0117df9b24e94e74899ba3185" }],
"notes": [{ "kind": "limitation", "subject": "user_store", "message": "当前离线模板刻意还原…" }],
"total_lines": 37
},
"artifacts": [
{ "kind": "source_code", "path": "user_store.py", "description": "37 行" },
{ "kind": "draft_manifest", "path": "draft_manifest.json", "description": "草案清单:文件、策略、开放问题" }
],
"metrics": { "elapsed_seconds": 0.004, "files": 1, "total_lines": 37, "token_usage": {} },
"diagnostics": { "engine": "offline", "profile": "realistic", "generation_mode": "template",
"model": "", "spec_warnings": [] },
"error": ""
}子进程协议有个硬要求:Worker 的 stdout 只允许出现那一个 JSON 对象,日志一律走 stderr。两种
模式都可用,--response-file(文件显式 UTF-8,编排器用的就是这条)和 stdout 直出(入口已经
把输出重配成 UTF-8)。stdout 模式在 Windows GBK 控制台下坏过一次,现在有回归测试守着,经过
写在 Design.md 里"开发中踩到的问题"一节。
python run_tests.py # 48/48
python run_tests.py -v # 逐个列出用例名
python run_tests.py protocol # 只跑文件名含 protocol 的用例(9/9)测试套件 coding-agent (Python 3.14.7)
目录:D:\软件工程实践\homework2\coding-agent\tests
──────────────────────────────────────────────────────────────────────
结果:48/48 通过,全部通过
run_tests.py 用标准库实现了用例发现和执行,测试函数就是普通的 test_xxx(),不装 pytest
也能跑,装了也兼容。48 个用例没有一个访问网络,也不需要 API Key。
test_templates.py 断言的是模板的性质,不是逐字比对源码,逐字比对会让任何一次排版调整都
变成改断言。它守四条性质:输出是合法 Python、realistic 确实埋了 7 类问题、clean 确实
没有、同输入必得同输出。每条断言都注明对应哪条审查规则(CG106 素材、CG105 素材……),
所以"改模板"和"审查还能不能检出"是耦合在一起的。
test_agent.py 里有两条回归测试是真被缺陷逼出来的:test_result_records_spec_provenance
(结果必须留下"这份代码从哪份需求生成")和 test_worker_stdout_mode_survives_non_ascii
(真的起子进程跑 stdout 模式,断言 stdout 是合法 UTF-8)。都在 Design.md 的"开发中踩到的问题"一节。
模板只覆盖 6 类函数语义,语义归类靠函数名前缀,create_user 能认,make_a_user 就退化;
只校验语法和体积,不校验语义;不能多轮澄清,需求写不清楚只能生成占位加开放问题;一次只出
一个文件,没有增量生成。这些都记在 Design.md 的"已知局限与改进方向"一节,附了改进方向。
没有 API Key 也能跑,默认 engine=offline,上面所有命令都不需要 Key、不联网。
render 说"不写磁盘",日志里却会出现临时目录:因为它复用同一条"生成 → 校验 → 落盘"的
路径(校验需要真实文件系统),落到系统临时目录后立即删除,--json 里的 output_dir 就是
那个路径。
生成的代码能不能直接用?--profile clean 那份是合规参照物;realistic 的明确不该当生产
代码,它是给审查 Agent 的输入。
需求没写模块名时按标题 slug 推断并记一条 warning,不会失败,用 spec 子命令可以看解析结果。
函数名不被模板识别时生成 raise NotImplementedError("R? … 待实现") 占位,原因登记进
open_questions,不去瞎猜实现。
生成的目录不会被提交,.gitignore 已忽略 worktree/、worktree-*/、job.json、
out.json;换个目录名得自己加。
能生成 Python 以外的语言吗?白名单允许 .js/.ts/.java/.go/.rs 等 28 种后缀,engine=llm
时可以生成,但离线模板只产出 Python,ast.parse 校验也只对 .py 生效。
换模型服务商不用改代码,改 CODING_BASE_URL、CODING_MODEL、CODING_API_KEY 即可,
协议是 OpenAI 兼容的。
MIT 许可,见 LICENSE。
| 仓库 | 说明 |
|---|---|
| https://github.com/0x697551b0/hw2-coding-agent | 本仓库:编码 Agent |
| https://github.com/0x697551b0/hw2-review-agent | 审查 Agent(复用 Homework 1 的 codeguard) |
| https://github.com/0x697551b0/hw2-fix-agent | 修复 Agent |
| https://github.com/0x697551b0/hw2-code-review-workflow | 工作流编排器 |
| https://github.com/0x697551b0/codeguard | Homework 1:CodeGuard 代码审查 Agent |
分层、核心机制、失败处理、开发中踩过的坑,都在 Design.md。