OpenSpec 最重要的工程价值,不是生成 Markdown,而是让一次变化能够从“为什么做”一路追到“哪段代码兑现、哪组测试证明、当前规格怎样更新、历史记录在哪里”。如果任何一层断开,文档数量再多也只是形式完整。
📷 [图片 token=FrvobgHLsoxDK7xKK0Dc591Rnod(未能下载,见飞书原文)]
追踪链到底在追什么
问题与边界
→ 可观察行为
→ 技术决策
→ 实施任务
→ 生产代码
→ 测试与运行证据
→ 当前主规格
→ 归档决策历史
→ 可浏览 WIKI
这里追踪的是语义,不是简单的文件链接。proposal 中的一条目标可能对应多个 Scenario;一个 Scenario 可能需要契约测试、服务测试和前端测试共同证明;一项 design 决策也可能被多个模块复用。因此追踪关系通常是一对多或多对多,不应强行要求“一条需求等于一个函数”。
📷 [图片 token=Ckseblw7qokwVKxyvanc0MWsnye(未能下载,见飞书原文)]
用 Embedding 批量限制案例建立矩阵
案例目录是 openspec/changes/archive/2026-07-11-limit-qwen-embedding-batch-size/。它的语义链可以整理为:
| 层次 | 核心问题 | 仓库证据 |
|---|---|---|
| Proposal | 为什么要改,边界在哪里 | proposal.md:百炼单次最多 10 条,范围限定为 Provider 与测试 |
| Delta spec | 系统必须表现成什么样 | specs/qwen-openai-provider/spec.md:1 条 Requirement、3 个 Scenario |
| Design | 为什么由这一层实现 | design.md:在 Provider 设置 chunk_size=10,业务层保持完整列表调用 |
| Tasks | 交付要产生哪些证据 | tasks.md:实现、客户端测试、索引回归和质量门禁 |
| 实现 | 技术决策落在哪里 | apps/backend/src/super_ai/llm/provider.py |
| 边界测试 | Provider 是否真的按 10 条分批并保序 | apps/backend/tests/test_llm_provider.py |
| 业务回归 | 大文档是否仍完整索引 | apps/backend/tests/test_document_indexing.py |
| Main spec | 当前系统正式承诺什么 | openspec/specs/qwen-openai-provider/spec.md |
| Archive | 这次为什么这样决策 | 带日期的完整归档 change |
| WIKI | 如何让历史便于浏览 | docs/changes/archive/.../index.md 通过 @include 引用原文件 |
第一段:从 proposal 锁定问题与范围
proposal 说明:较大文档会产生 11 个以上 chunk,默认 Embedding 客户端把它们作为一个请求发送,超过百炼 text-embedding-v4 的 10 条上限并得到 HTTP 400。它同时把影响范围限定为 Qwen Provider 构造、Provider 测试和文档索引回归。
📷 [图片 token=CjMObidddoEe8Mx6GeqcGHsJneb(未能下载,见飞书原文)]
这个边界很重要。没有 proposal,开发者可能去修改 chunking 策略、给索引服务加入厂商判断、调整 Milvus schema,甚至改变前端错误提示。proposal 中的非影响范围明确排除了 HTTP、SSE、Milvus schema 和前端,从一开始就减少无关改动。
追踪审查的第一个问题是:后续 spec、design 和代码有没有超出这一范围。如果出现数据库迁移或前端改动,就必须解释它为何必要,并回到 proposal 更新影响分析。
📷 [图片 token=IoLvbqn1PoA9KMxEJMlcf6uCnvc(未能下载,见飞书原文)]
第二段:把目标改写为可验收 Scenario
delta spec 没有写“修复 Embedding 报错”这种不可验收描述,而是拆成三个场景:
| Scenario | 需要证明的行为 | 主要证据 |
|---|---|---|
| 输入不超过 10 条 | 单个请求仍符合上限 | Provider 客户端配置与调用测试 |
| 输入超过 10 条 | 拆成每批最多 10 条,完整返回且顺序一致 | 11 条输入形成 10+1 请求,输出仍按原序 |
| 文档超过 10 个 chunk | 索引任务成功并写入全部 chunk | 索引服务回归测试 |
📷 [图片 token=KYR2bQsUroEPpZxqF0lcn5xznud(未能下载,见飞书原文)]
这里能看到“一个测试无法证明全部设计”。Provider 测试负责证明真实客户端配置、10+1 分批和顺序保持;索引回归使用 FakeEmbeddingModel 与 FakeVectorStore,证明业务层仍提交 11 个 chunk 并完整写入。后者没有真正执行 10+1 请求,前者也没有覆盖整个索引编排,两项证据组合后才与三个 Scenario 对齐。
📷 [图片 token=I8CIbW0kiof2hVxIwUlcnQ2Rnwb(未能下载,见飞书原文)]
第三段:让 design 解释责任归属
design 的关键决策是把提供商上限放在 OpenAIEmbeddings Provider 配置,而不是文档索引服务。其理由可以从三个方向追踪:
**抽象边界。**批量上限来自模型提供商,应由 Provider 隐藏;业务服务只关心“为这组文本生成向量”。
**复用范围。**未来其他调用方使用同一 Embedding Provider 时,无须重复实现拆批。
**变化成本。**提供商未来调整上限时,只修改 Provider 配置或配置项,而不是修改所有业务调用路径。
📷 [图片 token=MbNwbnyA3olsGdxEq8gcCadrnkh(未能下载,见飞书原文)]
代码中的 QWEN_EMBEDDING_BATCH_SIZE = 10 和 OpenAIEmbeddings(chunk_size=...) 正是这项决策的落点。如果代码改成索引服务手写分批循环,即使测试通过,也会违反 design 的责任边界,verify 应把它识别为一致性问题。
📷 [图片 token=QMxqbIQCJoLnkhxFhaxcfja5nyb(未能下载,见飞书原文)]
第四段:tasks 把行为与证据排成执行顺序
本案例的 tasks 没有按“后端开发、测试、结束”粗略分组,而是依次要求:设置默认 Qwen Embedding 的单批上限;验证真实客户端配置与超过上限的行为;验证 11 个以上 chunk 的索引回归;运行 Ruff、Pyright、Pytest 与 OpenSpec 校验。
📷 [图片 token=QDCNbE2bgowRH3x3cskc2l5OnSf(未能下载,见飞书原文)]
这种拆法让每个 checkbox 都有对象和证据。apply 可以按顺序推进,verify 也能反查“任务是否只勾选但没有对应实现或输出”。不过复选框仍不是证据本身,历史 tasks 中的 [x] 不能代替当前会话的测试结果。
📷 [图片 token=ZMRebzxUQoOslQxH4cHcFX7Nndb(未能下载,见飞书原文)]
第五段:从代码反向追踪到需求
正向追踪用于交付,反向追踪用于维护。假设未来 test_default_embedding_model_preserves_raw_qwen_inputs_and_batches_by_ten 失败,可以沿下面路径反查:
失败测试
→ provider.py 的 chunk_size 配置
→ design 中的 Provider 责任归属
→ delta spec 的大批量拆分 Scenario
→ proposal 中的百炼 10 条限制与范围
📷 [图片 token=AE7nbg7w7ou9E7xYDt3cqXNwncc(未能下载,见飞书原文)]
这样维护者不只知道“哪个断言坏了”,还能知道这个断言保护的是哪项系统承诺,以及为什么不能随意删除。反向链也能判断需求是否已经变化:如果提供商官方上限改变,应创建新 change 更新当前规格,而不是简单删掉测试。
Main spec、archive 与 WIKI 各自保存什么
Main spec 保存当前事实。openspec/specs/qwen-openai-provider/spec.md 已包含批量兼容 Requirement 和三个 Scenario。日常开发需要确认系统现在承诺什么,应先看这里。
**Archive 保存变化过程。**归档目录保留当时的 proposal、design、tasks、delta spec 和元数据。需要回答“为什么选择 Provider 层”“当时排除了哪些范围”,应查 archive。
WIKI 提供浏览入口。docs/changes/archive/2026-07-11-limit-qwen-embedding-batch-size/index.md 不复制正文,而是通过 @include 引用归档文件。它改善导航,但不成为第二套事实来源。
📷 [图片 token=JLHDb5Bm3ovVaax6XVzcNCIinng(未能下载,见飞书原文)]
OpenSpec 追踪与 Git 追踪并不重复
该案例的 archive、实现、测试和 main spec 可以在 Git 历史中找到,但相关提交同时包含很多其他文件。Git 能精确显示文本差异,却不天然提供一个聚焦于本变更的语义边界。OpenSpec change 把这次需求的意图、行为和取舍单独组织起来,二者互补。
也不能反过来夸大 OpenSpec:当前仓库没有把 Requirement ID 自动写进代码和测试,也没有自动生成形式化 trace matrix。现有追踪依赖稳定的 capability/Requirement 名称、文件路径、测试名称,以及开发者或 Agent 的语义核对。它是可审查的工程追踪,不是数学证明。
📷 [图片 token=XeWNbqPevoYayDxjS0RcEK4cnbb(未能下载,见飞书原文)]
审查一条追踪链的实用方法
从 proposal 选一条目标,确认 delta spec 中有对应 Requirement 或 Scenario。
确认 design 解释了实现责任、关键取舍和非目标。
从 Scenario 反推出 tasks,检查实现、测试和验证是否都被安排。
在生产代码中找到 design 决策的落点,而不只搜索相同关键词。
检查测试是否覆盖条件、结果和重要失败边界,并说明 fake 与真实集成的差别。
确认 delta 已正确进入 main spec,未覆盖无关 Requirement。
确认 archive 保留完整产物,WIKI include 指向归档后的真实路径。
📷 [图片 token=AGbObyD0YoKHNXx2WaHcqp1bnef(未能下载,见飞书原文)]
一段可直接使用的技术表达
我不会把 OpenSpec 理解成模板生成器,而是把它当作需求到证据的追踪链。在 Embedding 批量限制案例里,proposal 先锁定百炼 10 条上限和 Provider 层范围;delta spec 把小批量、大批量保序和完整索引写成三个 Scenario;design 决定由 Provider 配置 chunk_size;tasks 安排实现、边界测试、业务回归和质量门禁;代码与两层测试分别证明拆批和索引完整性;最后 delta 合入 main spec,完整 change 归档,WIKI 通过 include 提供浏览入口。这样任何人都能从当前行为反查当时的决策,也能从失败测试定位它保护的业务承诺。
📷 [图片 token=DmHfboHlwoVRv6xbpv7c2eJ3ndm(未能下载,见飞书原文)]