让领域层不知道模型是谁:AI 能力的适配层
今年做的法规管理平台里有两个 Python 子服务:一个把法规 PDF 切成条目,一个做法规翻译和术语库。两个服务的模型底座在开发期间换了三次——先是本地部署的开源模型,后来换成云上的,中间还试过一家国产的。领域层一行代码没改。这篇写为什么。
五个端口
一开始就把 AI 能力拆成五种接口,领域层只依赖接口:
| 端口 | 职责 |
|---|---|
| DocumentParser | 文件 → 结构化区块 |
| OcrEngine | 图片 → 文本与坐标 |
| Chunker | 区块 → 切片 |
| Embedder | 文本 → 向量 |
| LlmClient | 提示词 → 文本 |
每个端口在基础设施层有真实现(对接具体模型服务),也有假实现——返回固定结果或者简单规则的结果。
假实现不是为了测试
假实现当然能跑单元测试,但更重要的用途是:模型底座没定的时候,整条业务流程就能端到端跑通。
法规平台的业务流程很长:导入法规 → 条目化 → 关联车型 → 生成符合性任务 → 分配 → 填报 → 审批。条目化那一步要模型,但其他步骤不要。有了假实现,模型选型还在扯皮的时候,其他六步已经在联调了。
后来真实现接上,替换的是一个配置项。
领域层零框架依赖
Python 侧的领域层不 import 任何模型 SDK、任何 HTTP 库、任何数据库驱动。只有纯数据结构和业务规则。这在 Python 项目里不太常见——Python 项目习惯从上到下一把梭。
坚持这一点的收益在换模型的时候体现出来:换底座只动基础设施层的一个适配器,测试只跑那个适配器的集成测试,领域层的测试不用动。
一个具体的换法
换到云上模型时,接口协议变了(一家是 OpenAI 兼容的,一家不是)。做法:
- 新写一个
LlmClient实现。 - 配置切到新实现,跑一遍”金标准”用例集——十几份法规 PDF,人工核对过条目化结果。
- 对比新旧实现的输出差异,差异集中在哪几类条款。
- 差异在可接受范围,切;不在,回滚配置,一分钟的事。
适配层的代价
多一层抽象,多一份代码。五个端口每个两套实现,加起来不少。
而且端口的粒度要拿捏:切太细(比如 Embedder 再拆成”批量”和”单条”),每次换模型要改好几个;切太粗(把 Parser 和 OCR 合一个),换 OCR 引擎就得连 Parser 一起换。现在这五个是改了两次才稳定的。
什么时候不值得
如果模型底座已经定死、项目周期短、后面不会换,这层可以省。我做的这几个项目,没有一个满足”不会换”——模型迭代太快,半年就有更好的。所以我默认加这一层。