本页把 2026-07-11-limit-qwen-embedding-batch-size 从问题、产物、实现、测试、主规格到 WIKI 串成一条完整证据链。这个 change 规模很小,却包含边界定义、分层设计和多层验证,适合用来说明 OpenSpec 不是“写文档”,而是怎样控制一次真实工程变化。

📷 [图片 token=IdlEbXy31o2av9xulTSczAEJn4d(未能下载,见飞书原文)]

问题从哪里来

OncallAgent 支持 Markdown/PDF 知识文档上传、切分、Embedding、Milvus 写入和后续 RAG 检索。文档索引服务会把拆分后的完整 chunk 列表交给 EmbeddingModel.aembed_documents

阿里云百炼 text-embedding-v4 的 OpenAI-compatible 接口限制单次最多 10 条文本。默认客户端批量值更大时,一个产生 11 个以上 chunk 的文档会在向量生成阶段收到 HTTP 400,导致索引失败。

📷 [图片 token=G85mbkAaZoSgq6xDvayc2iLBnf7(未能下载,见飞书原文)]

注意,这个问题的表象是“大文档索引失败”,但根因不在切分算法,也不在 Milvus,而在模型 Provider 对上游批量约束适配不完整。准确归因决定了 change 应该修改哪一层。

📷 [图片 token=MtcKb7uvmo9o6MxsHNgcVsddnuf(未能下载,见飞书原文)]

.openspec.yaml:声明工作流

schema: spec-driven
created: 2026-07-11

它说明该 change 使用 spec-driven 产物链。没有业务描述,没有模型密钥,也没有运行时配置。归档后继续保留,用于说明历史 change 的流程上下文。

proposal:先控制范围

Why 写清 10 条限制、当前一次提交全部 chunk、11 条以上触发 HTTP 400 和索引失败。

What Changes 要求单批限制为 10,超过 10 自动分批,输入输出顺序一致,并增加回归测试。

Capabilities 没有新增能力,只修改已有 qwen-openai-provider

Impact 锁定后端 Embedding 构造、Provider 单测和索引回归测试;明确不修改 HTTP API、SSE、Milvus schema 和前端。

这一步最重要的作用是防止范围膨胀。修复不需要重新设计知识库页面,也不需要让索引 API 暴露 batch size。

📷 [图片 token=PiMQbedFVoZVecxgTp2cvnJMnif(未能下载,见飞书原文)]

delta spec:把成功写成可验收行为

delta ADDED 了 Qwen embedding batch compatibility,定义三个 Scenario:

**小输入。**不超过 10 条时,在一个兼容请求中完成。

**大输入。**超过 10 个 chunk 时,每批最多 10 条,并按输入顺序返回每个向量。

**最终业务结果。**可访问文档超过 10 个有效 chunk、Embedding 与 Milvus 可用时,全部向量和 chunk 必须写入,任务状态为 succeeded。

第三个场景很关键。只验证请求拆批还不够,最终用户关心的是索引任务完整成功,不能出现 11 个输入只保存前 10 个的静默数据丢失。

📷 [图片 token=SfNCbZTsAoPmhPxzYHoc2xO9nXf(未能下载,见飞书原文)]

design:决定约束归属

方案最终在 OpenAIEmbeddings 构造时设置 chunk_size=10。理由是批量上限属于 Provider 约束,应该由 Provider 统一适配,让所有调用方共享安全行为。

索引服务继续一次调用完整 chunk 列表。LangChain 客户端负责拆分上游请求并按原顺序合并响应。业务层依赖稳定的 EmbeddingModel 抽象,不需要知道百炼是 10 条、其他供应商又是多少条。

Trade-off 是大文档会产生更多 HTTP 请求,速度可能降低;但索引是异步后台任务,当前优先正确性。若未来上限提高,固定 10 会牺牲吞吐,可以再通过新 change 配置化。

📷 [图片 token=Dz2MbrlDyoJHofxoAK1csgm8nyh(未能下载,见飞书原文)]

tasks:把设计变成四个证据点

1.1 默认 Qwen 客户端单批限制为 10
1.2 增加默认客户端批量配置和行为单测
2.1 增加超过 10 chunk 的索引回归测试
2.2 运行 Ruff、Pyright、Pytest、OpenSpec 验证

前两项证明 Provider 适配,第三项证明业务结果,第四项完成质量门禁。归档中的复选框均为完成状态,但它们是历史记录;

📷 [图片 token=EH8ubXqP8ocHBgxLLS7cwRtVn2g(未能下载,见飞书原文)]

实现:变化集中在 Provider

当前代码在 apps/backend/src/super_ai/llm/provider.py 定义:

QWEN_EMBEDDING_BATCH_SIZE = 10

构造 OpenAIEmbeddings 时传入:

chunk_size=QWEN_EMBEDDING_BATCH_SIZE
check_embedding_ctx_length=False

这与 design 一致:适配留在 Provider,索引服务没有新增厂商分支。check_embedding_ctx_length=False 还保证 OpenAI-compatible Qwen 接收原始字符串输入,而不是被默认逻辑转换成 token ID 数组。

📷 [图片 token=HrrgbG8z5oHMhKxtZhJcpVzUnKf(未能下载,见飞书原文)]

测试:为什么需要两层

Provider 行为测试 创建默认 Embedding 模型,用 11 条文本调用,并替换底层异步客户端收集真实请求。断言 chunk_size == 10,请求输入是 [前10条, 后1条],最终向量仍按 0 到 10 顺序返回。它直接证明拆批发生在 Provider/LangChain 层。

索引服务回归测试 用 11 个字符生成 11 个单字符 chunk,断言索引任务 succeeded,业务层一次交给 fake Embedding 11 条,向量库最终插入 11 个 chunk。这个测试故意不在业务层模拟 10+1,因为 design 要求业务层不感知厂商上限。

两类测试并不重复:一个验证适配细节,一个验证业务完整性。把它们混成单一大测试,失败时反而难以判断是拆批、顺序、索引状态还是存储出了问题。

📷 [图片 token=A7cpbXiWCobPI2x4cB7c9Mqjnrg(未能下载,见飞书原文)]

sync 与 archive:从变化变成当前事实

delta 已合入 openspec/specs/qwen-openai-provider/spec.md,主规格现在包含同名 Requirement 与三个 Scenario。随后整个 change 进入:

openspec/changes/archive/
2026-07-11-limit-qwen-embedding-batch-size/

主规格告诉新任务“当前 Provider 必须兼容 10 条上限”;archive 则保留当时为什么这样做、有哪些非目标、承担哪些代价和怎样验证。

📷 [图片 token=XBH5bqRVXoiGISx19MVcRqGOnHe(未能下载,见飞书原文)]

wiki-sync:把历史变成可浏览页面

仓库生成了:

docs/changes/archive/
2026-07-11-limit-qwen-embedding-batch-size/index.md

聚合页包含 archived frontmatter,通过 @include 引用 proposal、design、tasks 和 qwen-openai-provider delta spec;总索引和 Sidebar 也包含同一条目。正文没有被复制,OpenSpec 仍是唯一事实来源。

📷 [图片 token=FbGSb2F64o7YGvxfDnFcN5H5nLb(未能下载,见飞书原文)]

这段经历怎样讲得可信

我们遇到的不是一般“大文档性能问题”,而是 Provider 未适配百炼 Embedding 单次 10 条上限。先用 proposal 排除 API、Milvus 和前端变化,再用 spec 同时约束请求上限、顺序完整性和最终索引成功。设计上把 chunk_size 放在 Provider,让业务层继续提交完整列表;测试则分成 Provider 10+1 拆批与索引 11 chunk 完整落库两层。归档后 delta 进入主规格,wiki-sync 通过 include 展示完整决策链。这说明修复的不只是一个 400,而是把供应商约束收敛在正确边界。

可信的关键是能说明因果链、边界、取舍和证据,而不是堆砌“规范驱动”“Agent 自动化”等词语。

📷 [图片 token=QPoPbGVyuoYE6Vx5ld6cMOPtngb(未能下载,见飞书原文)]