Skip to content

About

软件工程实践 Homework 2 · 编码 Agent:把 Markdown 需求翻译成可运行的代码草案(离线模板引擎 + LLM 双引擎)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

CodingAgent

把一份 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-plus

render 出来的东西长这样(真实输出,中间省略了 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   # 退出码 0

out.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。

About

软件工程实践 Homework 2 · 编码 Agent:把 Markdown 需求翻译成可运行的代码草案(离线模板引擎 + LLM 双引擎)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages