新闻详情

新闻详情

首页 / 资讯中心 / 详情

Harness Engineering 从入门到精通:用 AGENTS.md 与 Lint 搭建 SDD/TDD 工程骨架

发布时间:2026/9/26 18:09:18来源:尧图网络
Harness Engineering 从入门到精通:用 AGENTS.md 与 Lint 搭建 SDD/TDD 工程骨架
1. 为什么你的 AI 编码总是跑偏先说一个我踩过的坑。去年我用某个 AI 编码工具做一个股票数据小工具需求其实不复杂输入股票代码抓几个维度的数据输出一份结构化报告。我一开始的做法很氛围编程——把需求用三五句话描述给 AI让它直接开写。第一版跑通了我挺开心第二天想加个新维度AI 顺手把原来的报告结构也改了字段名从fundamental变成了basic_info下游解析全崩。我又花了两小时把它拽回来。问题不在于模型不够聪明而在于我从来没告诉它什么叫做对了。需求在我脑子里AI 看不到约束我没写下来AI 记不住质量我没设门禁AI 每次都能自由发挥。这就是 Harness Engineering驾驭工程要解决的事——它不是某个工具而是一套让 AI 在明确边界内稳定输出的工程方法。Harness Engineering 的核心可以拆成三根支柱告知Inform、约束Constrain、验证Verify。告知靠AGENTS.md这类导航文件让 AI 知道去哪找规则约束靠 Lint 规则和架构边界让 AI 知道什么不能做验证靠测试和结构检查器让 AI 知道做得对不对。三者串起来就形成了 SDD规格驱动开发到 TDD测试驱动开发的完整闭环。这篇文章适合谁如果你已经在用 AI 写代码但经常遇到改一处崩三处输出格式每次都不一样AI 复制了仓库里的坏习惯这类问题那这篇就是给你写的。我会用一个可复制的股票研究工具骨架把AGENTS.md、自定义 Linter、pytest 测试用例、CI 质量门禁全部串一遍最后演示一次从需求到测试的完整验证动作。全程可跟做代码片段直接能跑。2. 前置准备把模型接入和 Key 管起来在动手写 Harness 之前得先有一个稳定的模型调用入口。Harness 里的验证环节经常需要真实调用模型来跑集成测试所以这一步不能省。我这边统一用 TaoToken 来做模型接入。它的官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。选它的原因很简单Harness 工程里最怕的就是环境不稳定导致测试偶发失败而一个统一的 API 网关能让你的qwen_client.py只依赖一个 base_url 和一个 key换模型、换环境都不用改业务代码。具体操作分三步。第一步去控制台创建一个 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。创建完把 key 复制出来不要写进代码而是放进环境变量。第二步如果你只是想先验证模型能不能通可以直接用模型对话页面试一句地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。第三步把 key 落到本地环境变量里# Linux / macOS export TAOTOKEN_API_KEYsk-你的key # Windows PowerShell $env:TAOTOKEN_API_KEYsk-你的key然后在项目里写一个最小的客户端封装。注意 Harness 的一条硬规则API 调用只能发生在一个文件里其他模块一律不直接碰外部 API。这样做的目的是让外部依赖成为一个可被 mock 的边界单元测试才能做到零网络。# src/qwen_client.py import os from openai import OpenAI class QwenClient: 所有外部 API 调用的唯一出口。其他模块禁止直接 import openai。 def __init__(self, base_url: str https://taotoken.net/api, api_key: str | None None): self.client OpenAI( base_urlbase_url, api_keyapi_key or os.environ[TAOTOKEN_API_KEY], ) def chat(self, prompt: str, model: str qwen-plus) - str: resp self.client.chat.completions.create( modelmodel, messages[{role: user, content: prompt}], temperature0.2, ) return resp.choices[0].message.content这里有个细节值得说temperature0.2是刻意压低的。Harness 追求的是可预测性不是创意。报告生成这种任务你希望同样的输入尽量得到结构一致的结果低温度能显著减少格式漂移。如果你后面要做长期的编码 Agent 或者让 AI 在循环里自主干活可以考虑用 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它更适合高频、长时运行的场景。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到参数问题先翻这里。3. 可复制配置AGENTS.md 骨架 Lint 规则这一节是全文的核心。我会给你两份可以直接抄的配置一份是AGENTS.md一份是自定义 Linter。3.1 AGENTS.md地图不是手册先说一个反直觉的结论AGENTS.md写得越长效果越差。实践里超过 60 行AI 对它的遵循度就开始下降。原因有三个——挤占上下文窗口、难以维护、无法被机械验证。所以AGENTS.md的定位是图书馆门口的目录牌不是百科全书。它只回答一个问题你要找的东西在哪具体规则放在 Spec 和 Linter 里AGENTS.md只做导航。下面这份骨架你可以直接改项目名用# Stock Deep Research - AGENTS.md 本文件是项目导航入口给 AI Agent 和开发者看的目录页。 原则地图而非手册控制在 60 行以内指向更深层文档。 ## 项目定位 AI 驱动的股票深度研究工具输入股票代码输出多维度结构化研报。 同时作为 SDD TDD Harness Engineering 的教学案例。 ## 关键文件导航 | 文件 | 用途 | |------|------| | spec/research_spec.md | 规格文档一等公民所有约束的权威来源 | | src/qwen_client.py | 模型 API 客户端封装唯一外部调用出口 | | src/collector.py | 多维度数据采集 | | src/analyzer.py | 汇总分析 评分 | | src/reporter.py | 报告生成 结构校验 | | src/validator.py | 报告验证器C1-C8 约束 | | tests/test_validator.py | 测试用例 | | linters/check_structure.py | 项目结构 Linter | ## 开发约定 1. TDD 强制新功能先写失败测试再写实现 2. Spec 同步改报告结构必须同步更新 spec/research_spec.md 3. 测试隔离单元测试禁止调用真实 API一律 Mock 4. 结构对称src/ 下每个模块对应 tests/ 下同名 test_ 文件 ## 常用命令 bash pytest tests/ -q # 全部单元测试 pytest tests/test_validator.py -v # 单模块 python linters/check_structure.py # 结构检查 python src/main.py 600519 # 生成报告架构约束依赖方向client - collector - analyzer - reporter禁止反向依赖reporter 不能 import collectorAPI 调用只发生在 qwen_client.py核心代码全部放在 src/注意最后那段架构约束——它其实是从 Linter 里抄过来的摘要。真正强制执行的是 LinterAGENTS.md 只是让 AI 提前知道有这么回事。这就是告知和约束的分工。 ### 3.2 自定义 Linter把规范变成机械检查 文档会腐烂Lint 规则不会。写在文档里的规范AI 经常忘写成 Lint 规则的约束AI 每次都得遵守——因为违反会导致 CI 失败它跳不过去。 更关键的一点**Lint 的错误信息里要嵌入修复指令**。普通报错只说你错了AI 不知道怎么修如果报错里直接写清楚怎么改AI 就能自我纠正形成闭环。 下面是一个项目结构 Linter 的骨架重点看错误信息里的 FIX: 部分 python #!/usr/bin/env python3 项目结构校验工具。文档会腐烂lint 规则不会。 import ast import os import sys from pathlib import Path REQUIRED { src: [__init__.py, qwen_client.py, collector.py, analyzer.py, reporter.py, validator.py, main.py], tests: [__init__.py, test_validator.py], spec: [research_spec.md], } class StructureLinter: def __init__(self, root: str .): self.root Path(root) self.errors: list[str] [] def check_files(self) - None: for d, files in REQUIRED.items(): target self.root / d if not target.exists(): self.errors.append( fERROR: 目录 {d}/ 不存在。\n fFIX: 创建 {d}/ 目录并补齐 {, .join(files)}。 ) continue existing {p.name for p in target.iterdir()} for f in files: if f not in existing: self.errors.append( fERROR: {d}/ 缺少 {f}。\n fFIX: 在 {d}/ 下新建 {f}参考 spec/research_spec.md 的模块职责。 ) def check_reporter_has_validate(self) - None: reporter.py 必须暴露 validate_report 函数。 path self.root / src / reporter.py if not path.exists(): return tree ast.parse(path.read_text(encodingutf-8)) funcs [n.name for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)] if validate_report not in funcs: self.errors.append( ERROR: src/reporter.py 缺少 validate_report() 函数。\n FIX: 添加 def validate_report(report: dict) - list[dict]\n 逐条检查 spec/research_spec.md 中的约束 C1-C8。 ) def run(self) - bool: print(正在进行项目结构校验...) self.check_files() self.check_reporter_has_validate() if self.errors: print(\n.join(self.errors)) return False print(项目结构校验通过。) return True if __name__ __main__: sys.exit(0 if StructureLinter().run() else 1)跑一下python linters/check_structure.py如果src/reporter.py里没有validate_report你会看到带FIX:的报错。AI Agent 读到这段输出就能自己补上函数——这就是约束和验证合体的效果。4. 验证请求从 Spec 到测试的完整动作配置写完了得证明它真的能拦住问题。这一节我演示一次完整的需求 → 测试 → 验证动作。4.1 先写 Spec把约束编号Spec 是 SDD 的一等公民。它的每一条约束都应该能直接翻译成一个测试用例。下面这份 Spec 我截取了约束部分# 股票深度研究 - 规格文档 本文档是项目的一等公民。所有实现代码都是本规格的可执行表达。 ## 输出格式 报告必须严格遵循以下 JSON 结构 { stock_code: 600519, stock_name: 贵州茅台, report_date: 2026-04-20, dimensions: { fundamental: {summary: ..., confidence: 0.85, akshare_data: true}, market: {summary: ..., confidence: 0.78, akshare_data: true}, news: {summary: ..., confidence: 0.72, akshare_data: true}, analyst: {summary: ..., confidence: 0.80, akshare_data: true} }, overall_rating: buy, risk_factors: [..., ...], sources: [https://..., https://..., https://...], akshare_version: 1.10.60 } ## 约束条件将直接转化为测试用例和 Lint 规则 - C1 维度完整性必须包含 fundamental/market/news/analyst 四个维度 - C2 摘要最小长度每个 summary 不少于 100 字符 - C3 置信度范围confidence 必须在 [0.0, 1.0] - C4 评级有效值overall_rating 只能是 buy/hold/sell - C5 来源数量sources 至少 3 个 URL - C6 风险因素risk_factors 不能为空 - C7 必填字段stock_code/stock_name/report_date 必填 - C8 AkShare 标记每个维度必须有 akshare_data 字段4.2 再写测试先让它红TDD 的红-绿-重构循环里红这一步最容易被跳过。但它的价值恰恰在于证明测试本身是有效的。一个从没失败过的测试等于没有测试。# tests/test_validator.py import pytest from src.validator import StockReportValidator def make_valid_report() - dict: return { stock_code: 600519, stock_name: 贵州茅台, report_date: 2026-04-20, dimensions: { fundamental: {summary: 基本面分析 * 20, confidence: 0.85, akshare_data: True}, market: {summary: 市场面分析 * 20, confidence: 0.78, akshare_data: True}, news: {summary: 消息面分析 * 20, confidence: 0.72, akshare_data: True}, analyst: {summary: 分析师观点 * 20, confidence: 0.80, akshare_data: True}, }, overall_rating: buy, risk_factors: [行业周期风险, 估值波动风险], sources: [https://a.com, https://b.com, https://c.com], akshare_version: 1.10.60, } class TestC1DimensionCompleteness: def test_all_dimensions_present(self): v StockReportValidator(make_valid_report()) assert v.validate() is True assert v.get_errors() [] def test_missing_dimension_fails(self): report make_valid_report() del report[dimensions][news] v StockReportValidator(report) assert v.validate() is False assert any(C1 in e for e in v.get_errors()) class TestC3ConfidenceRange: def test_confidence_out_of_range_fails(self): report make_valid_report() report[dimensions][market][confidence] 1.5 v StockReportValidator(report) assert v.validate() is False assert any(C3 in e for e in v.get_errors()) class TestC4RatingValid: def test_invalid_rating_fails(self): report make_valid_report() report[overall_rating] strong_buy v StockReportValidator(report) assert v.validate() is False assert any(C4 in e for e in v.get_errors())现在src/validator.py还不存在跑测试必然是红的pytest tests/test_validator.py -v # ModuleNotFoundError: No module named src.validator这个红是扳机——它确认你正在解决一个真实存在的问题而不是臆想。4.3 写实现让它变绿# src/validator.py class StockReportValidator: REQUIRED_DIMENSIONS [fundamental, market, news, analyst] VALID_RATINGS {buy, hold, sell} MIN_SUMMARY_LEN 100 def __init__(self, report: dict): self.report report self.errors: list[str] [] def get_errors(self) - list[str]: return self.errors def validate(self) - bool: self.errors [] self._c1_dimensions() self._c2_summary_length() self._c3_confidence() self._c4_rating() self._c5_sources() self._c6_risks() self._c7_required() self._c8_akshare() return not self.errors def _c1_dimensions(self): dims self.report.get(dimensions, {}) missing [d for d in self.REQUIRED_DIMENSIONS if d not in dims] if missing: self.errors.append( fC1: 缺少维度 {missing}。\n fFIX: 在 dimensions 中补齐 {self.REQUIRED_DIMENSIONS}。 ) def _c2_summary_length(self): for name, d in self.report.get(dimensions, {}).items(): if len(d.get(summary, )) self.MIN_SUMMARY_LEN: self.errors.append( fC2: 维度 {name} 的 summary 少于 {self.MIN_SUMMARY_LEN} 字符。\n fFIX: 补充该维度的分析内容确保信息充分。 ) def _c3_confidence(self): for name, d in self.report.get(dimensions, {}).items(): c d.get(confidence) if not isinstance(c, (int, float)) or not (0.0 c 1.0): self.errors.append( fC3: 维度 {name} 的 confidence{c} 超出 [0,1]。\n fFIX: 将评分逻辑归一化到 0~1 区间。 ) def _c4_rating(self): r self.report.get(overall_rating) if r not in self.VALID_RATINGS: self.errors.append( fC4: overall_rating{r} 非法。\n fFIX: 只能取 {sorted(self.VALID_RATINGS)} 之一。 ) def _c5_sources(self): if len(self.report.get(sources, [])) 3: self.errors.append(C5: sources 少于 3 个。\nFIX: 补充来源 URL。) def _c6_risks(self): if not self.report.get(risk_factors): self.errors.append(C6: risk_factors 为空。\nFIX: 至少列出 1 条风险因素。) def _c7_required(self): for f in [stock_code, stock_name, report_date]: if not self.report.get(f): self.errors.append(fC7: 缺少必填字段 {f}。\nFIX: 补齐该字段。) def _c8_akshare(self): for name, d in self.report.get(dimensions, {}).items(): if akshare_data not in d: self.errors.append( fC8: 维度 {name} 缺少 akshare_data 标记。\n fFIX: 添加 akshare_data: true/false。 )再跑一次pytest tests/test_validator.py -v # 5 passed绿了。注意每个错误信息里都带FIX:——这不是给人看的装饰而是给 AI Agent 看的修复指令。当 Agent 生成的报告不合格时它读一遍错误列表就能自己改。4.4 接上 CI 质量门禁最后把三道门禁串起来。门禁从快到慢排列越早发现问题修复成本越低# .github/workflows/quality_gate.yml name: Quality Gate on: push: branches: [main] pull_request: branches: [main] jobs: structure-lint: runs-on: ubuntu-latest timeout-minutes: 2 steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.11 - run: python linters/check_structure.py unit-tests: runs-on: ubuntu-latest timeout-minutes: 5 needs: structure-lint steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.11 - run: pip install -r requirements.txt - run: pytest tests/ -v --tbshort -m not integration env: TAOTOKEN_API_KEY: integration-tests: runs-on: ubuntu-latest timeout-minutes: 10 needs: unit-tests if: github.ref refs/heads/main steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.11 - run: pip install -r requirements.txt - run: pytest tests/test_integration.py -v -m integration env: TAOTOKEN_API_KEY: ${{ secrets.TAOTOKEN_API_KEY }}三道门禁的分工很清晰结构检查 2 分钟单元测试 5 分钟集成测试只在 main 分支跑、需要真实 Key。单元测试那一步故意把TAOTOKEN_API_KEY设成空字符串——这是测试隔离的强制手段任何偷偷调用真实 API 的单元测试都会当场失败。5. 本篇常见错排查跟做过程中最容易卡住的几个点我按出现频率排一下。报错一ModuleNotFoundError: No module named src跑 pytest 时找不到src包。原因是项目根目录没进sys.path。两种解法在项目根加一个conftest.py内容为空即可pytest 会自动把根目录加入路径或者在pyproject.toml里配置pythonpath [.]。我推荐前者零配置。报错二Linter 报缺少 validate_report 函数但代码里明明有大概率是函数定义在if __name__ __main__:块里或者被包在类里。Linter 用ast.walk扫的是模块级FunctionDef类方法虽然也能扫到但如果你写成了嵌套函数就会漏。检查一下缩进层级。报错三单元测试偶发失败重跑又过了这是最危险的信号说明有测试在偷偷访问网络。检查两点一是qwen_client.py有没有被 mock 掉二是环境变量里是不是残留了真实的TAOTOKEN_API_KEY。CI 里把 key 设成空字符串就是为了逼出这类问题。本地跑的时候可以临时unset TAOTOKEN_API_KEY验证一遍。报错四AI 生成的报告字段名和 Spec 对不上比如把overall_rating写成rating。这属于告知没做到位——AGENTS.md里要明确指向 SpecSpec 里要把 JSON 结构写全。如果还是漂移就在 Linter 里加一条字段名白名单检查把错误信息写成FIX: 字段名必须与 spec/research_spec.md 的 JSON 结构完全一致。报错五CI 里集成测试超时集成测试默认 10 分钟超时。如果模型响应慢先确认 base_url 是不是https://taotoken.net/api再确认模型名拼写。参数细节可以对照接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 核对。如果只是本地调试建议先用模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 手动发一条请求确认链路通了再跑测试。报错六改了 Spec 但测试没跟着变这是流程问题不是代码问题。解决办法是把Spec 同步写进AGENTS.md的开发约定并且在 CI 里加一条检查Spec 文件的 hash 变了但测试文件没变就报警。简单点的话靠 code review 卡住也行。6. 把 Harness 用起来从 API Key 到长期编码到这里一个最小可用的 Harness 骨架就跑通了AGENTS.md负责导航Linter 负责约束pytest 负责验证CI 负责兜底。你会发现工程师的核心产出正在从写代码变成设计让 AI 可靠工作的约束系统。接下来怎么落地取决于你的使用场景。如果你只是想把模型接进现有项目先去控制台拿一个 API Key地址是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 然后照着接入文档把qwen_client.py配好地址是 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你还在选模型、想先验证效果直接去模型对话页面发几条真实请求地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 比看文档快。如果你打算做长期的编码 Agent让它在循环里自主干活——也就是 Ralph 那种每轮清空上下文、靠文件传递状态的模式——那 Coding Plan 会更合适地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。它针对高频、长时运行的场景做了优化配合本文的 Linter 和测试门禁就能形成Agent 干活、Harness 兜底的稳定结构。最后留一个我自己的经验当 Agent 出错时不要急着改它的代码先改 Harness。补一条 Lint 规则、加一段AGENTS.md说明、增加一个测试用例。修 Harness 一次Agent 以后每次都做对。这才是驾驭工程真正的杠杆所在。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

2026年,AI Agent正在从“回答问题”走向“完成任务”:用TaoToken统一Key打通Planning与Memory 2026/9/26 18:48:13

2026年,AI Agent正在从“回答问题”走向“完成任务”:用TaoToken统一Key打通Planning与Memory

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Java 程序员第 49 阶段4:双向注意力 vs 单向因果掩码:一张表看懂差异 2026/9/26 18:48:13

Java 程序员第 49 阶段4:双向注意力 vs 单向因果掩码:一张表看懂差异

1. 为什么「双向注意力 vs 单向因果掩码:一张表看懂差异」值得 Java 工程师专门吃透 在大模型工程落地里,这个话题绕不开。很多 Java 同学刚接触时容易只看结论、不究原理,一旦线上出问题就无从下手。先把「为什么重要」说清楚,后…

阅读更多 →
游戏多选一且多次时的技巧 2026/9/26 18:48:06

游戏多选一且多次时的技巧

个人经验,仅供参考流程图案例:场景:支付宝游戏→灵画师→秘宝→铜器店次数:3次第1次:任选一个2(未命中)第2次:次数未用完→未命中→选择不变2(命中)第3次&…

阅读更多 →
GEO视角:生成式搜索如何改写企业内容生产与分发逻辑 2026/9/26 18:48:06

GEO视角:生成式搜索如何改写企业内容生产与分发逻辑

一、生成式搜索对企业线上可见的四个常见问题当AI搜索逐步替代传统关键词检索,企业线上可见度的底层逻辑正在被重写。第一,内容被AI采信的门槛变了,过去堆砌关键词就能获得排名的做法,在生成式引擎中几乎失效。第二,用…

阅读更多 →
如何降低论文AI率?从自己检测到修改、复检的完整攻略。 2026/9/26 18:48:06

如何降低论文AI率?从自己检测到修改、复检的完整攻略。

如何降低论文AI率?从自己检测到修改、复检的完整攻略。 论文查重已经过了,AI率却没有达到学校要求;你把标红段落换了一遍词,第二份报告仍然不好看。有的人这时开始不停换网站检测,有的人把全文丢给大模型反复重写&…

阅读更多 →
WorkBuddy定时任务实战:每天十点半自动推送AI日报到微信 2026/9/26 18:48:00

WorkBuddy定时任务实战:每天十点半自动推送AI日报到微信

1. 为什么我要给 WorkBuddy 设一个“十点半闹钟”每天早上到工位,第一件事不是泡茶,而是打开各种信息源翻一遍:项目群里有没有新需求、昨天提交的代码有没有异常、行业里又出了什么新工具。这套动作重复了几个月之后,我意识到它本…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉