design.md 负责回答“怎样实现,以及为什么选择这种实现”。它连接行为契约与代码结构:spec 只要求系统表现正确,design 则说明约束放在哪一层、哪些抽象保持不变、如何测试、接受什么代价。
面试中真正能体现工程判断的内容,往往不在“我改了某个参数”,而在“为什么参数应该由 Provider 层持有,而不是让每个业务调用方重复理解厂商限制”。这正是 design 应保存的知识。
📷 [图片 token=UXJgbvOWkohnTcx2QPpcy7hlnGc(未能下载,见飞书原文)]
推荐组织结构
## Context
## Goals / Non-Goals
## Decisions
## Risks / Trade-offs
## Migration Plan
## Open Questions
不是每个 change 都必须机械填满所有章节。小型修复可以没有 Migration Plan 和 Open Questions,但 Context、目标边界、关键决策和风险通常不可缺少。
📷 [图片 token=IITCbCzGyoPYyPxrkaZcQppTn5c(未能下载,见飞书原文)]
Context:给技术决策补足现场
Context 应解释当前架构、数据流、约束和失败位置。主案例的现场是:文档索引服务把拆分后的完整 chunk 列表一次交给 EmbeddingModel.aembed_documents;默认 OpenAIEmbeddings 的批量值超过厂商单次 10 条限制,因此 11 个以上 chunk 会收到 HTTP 400。
这段 Context 同时说明业务抽象和外部约束。没有它,读者只看到 chunk_size=10,会误以为这是随意调优;有了它,才能判断问题属于 Provider 适配,而不是文档切分或 Milvus 写入。
📷 [图片 token=K8vfbjmUJolATPxEcvicIaRYn5j(未能下载,见飞书原文)]
Goals / Non-Goals:约束设计空间
Goals 描述设计必须达到的技术目标。主案例要求每个真实请求不超过 10 条,索引服务仍可一次提交完整列表,客户端透明分批并保持向量顺序,同时用两层测试覆盖。
Non-Goals 明确本次不解决什么:不改 chunking 策略、不改 Milvus 写入格式、不改索引任务 API 和前端,也不为所有 Embedding Provider 建动态限流系统。
Non-Goals 不是“偷懒声明”,而是控制 change 的原子性。如果把动态限流、配置中心和所有供应商适配都塞进一个小修复,设计成本和回归面会突然扩大,反而延迟解决已经可复现的故障。
📷 [图片 token=JQlBbpeibojDyGxXKTrccjjunbA(未能下载,见飞书原文)]
Decisions:记录选项、理由和边界
主案例有三项互相关联的决定:
**在 Provider 构造处设置 chunk_size=10。**批量上限来自模型提供商,属于 Provider 适配责任。放在这里,文档索引、知识检索和未来调用方都自动获得同一兼容行为。
**保留业务层一次调用完整列表。**索引服务依赖稳定的 EmbeddingModel 抽象,不知道百炼的请求上限。LangChain 客户端负责分批和重组,业务代码不会被厂商细节污染。
**使用分层测试。**Provider 测试直接验证 11 条输入变成 10+1 请求并保持顺序;文档索引测试验证业务层一次提交 11 个 chunk,最终全部写入。前者锁定适配细节,后者锁定端到端业务结果。
📷 [图片 token=A1JDbpHGDoo3s6x1j8bcDSDsnhb(未能下载,见飞书原文)]
好的 Decision 不只是宣布结论,还要说明责任归属和为什么不选择另一层。这样未来重构时,维护者知道哪些边界是有意设计,哪些只是当时实现。
📷 [图片 token=Pgr7bzJWRosQebxQtzocApcOnBf(未能下载,见飞书原文)]
Risks / Trade-offs:承认方案代价
任何技术方案都有成本。主案例承认大文档会产生更多 HTTP 请求,索引耗时可能增加;但索引本来就是后台任务,正确性和兼容性优先。它也承认厂商未来放宽上限时固定 10 会牺牲吞吐,后续可以配置化。
这种写法比“方案无风险”更可信。Trade-off 的意义不是制造恐慌,而是告诉评审者:我们知道牺牲了什么,为什么目前可以接受,以及未来什么条件会触发重新设计。
📷 [图片 token=VXefbMoPOoCn53xQhn1cSTU0nxf(未能下载,见飞书原文)]
Migration Plan 什么时候需要
涉及数据库 schema、持久化格式、共享 API/SSE 契约、配置结构或外部服务切换时,应写迁移步骤、兼容窗口、回滚方案和数据修复。OncallAgent 的数据库变化必须增加 Alembic migration,不能直接修改已发布 migration 伪装当前状态。
主案例只调整 Provider 客户端构造,不改变存量数据和协议,所以不需要复杂迁移。这也是设计判断:没有迁移需求时不要为了模板完整虚构一套。
📷 [图片 token=RU7MbQQLRozMCXxVnIvcMW59n8g(未能下载,见飞书原文)]
Open Questions 怎样使用
Open Questions 保存尚未决定、但会影响方案的问题。它们应在进入相关实现前关闭,或者明确标注不阻塞当前范围。不能把关键安全、权限和数据一致性问题长期留在“以后再说”。
📷 [图片 token=NByGbKx9doCTFoxnaTpcyQJon2g(未能下载,见飞书原文)]
怎样审查 design
**是否与 spec 分工清楚。**design 可以写 LangChain、Provider、常量和测试位置;spec 仍只写可观察行为。
**决策是否符合仓库边界。**OncallAgent 要求 FastAPI 路由保持薄层、业务逻辑进入服务或 repository、项目配置只来自本地 JSON、用户资源显式 owner/tenant scope、MCP 真实调用并审计。设计不能绕过这些不变量。
**是否说明失败路径。**外部模型、Milvus、MCP、CLS 和 Alertmanager 都可能失败。设计必须返回真实失败并脱敏,不能编造成功状态。
**测试是否验证了正确层次。**配置断言、适配行为、业务结果和端到端交互各有边界,不要只写一个宽泛测试名称。
📷 [图片 token=LhFBb2jKzoNKxtxRShacpp72nBb(未能下载,见飞书原文)]
面试表达
design 不是代码清单,而是决策记录。Embedding 案例里,我把厂商 10 条限制收敛到 Provider 层,保留业务层的完整列表抽象,并用 Provider 10+1 拆批测试和索引 11 chunk 落库测试分别锁定适配行为与业务结果。这样不仅修了 HTTP 400,也避免厂商约束扩散到业务代码。