文章目录
- 一、从 ChatBI 大泥球看 DDD 与 COLA 的分工
- 二、DDD 与 COLA 的关系:业务治理地图与代码货架
- 三、ChatBI 案例:从胖 Service 到 DDD + COLA
- 四、回到架构思维:控制变化,而不是堆架构
- 结语
ChatBI 系统一开始通常很简单:用户输入一个问题,系统把问题交给大模型,生成 SQL,查询数据库,再把结果返回给用户。
这个流程看起来像一条直线,所以很多项目最初会把它写在一个ChatServiceImpl里。
后来需求越来越多:要识别指标口径,要做权限校验,要选择数据集,要改写用户问题,要生成 SQL,要校验 SQL,要查数仓,要解释图表,要记录会话,还要做安全审计。
于是一个 ChatBI 服务很快变成这样:
接收用户问题 识别意图 选择数据集 解析指标口径 拼 Prompt 调用大模型 生成 SQL 校验 SQL 检查权限 执行查询 生成图表 组织自然语言回答 记录会话 埋点审计代码还能跑,但系统已经开始变重。问题不只是方法太长,而是几类完全不同的复杂度被塞到一起了。
ChatBI 的复杂度至少有四类:
入口复杂度:Web、API、消息、插件入口 流程复杂度:一次问答要经过多个步骤 业务复杂度:指标、口径、权限、数据集、问答语义 技术复杂度:LLM、向量库、数仓、缓存、审计、消息如果这些都挤在一个ChatServiceImpl里,任何变化都会牵动它:模型换了要改,指标口径变了要改,权限规则变了要改,SQL 引擎换了也要改。
这就是架构开始失控的地方。
DDD 和 COLA 的价值,要从这里理解:
DDD 解决:ChatBI 的业务概念和规则怎么建模。 COLA 解决:这些模型、流程和技术实现应该放在哪里。先看清业务,再安放代码。
一、从 ChatBI 大泥球看 DDD 与 COLA 的分工
ChatBI 最容易写成一个“智能问答大方法”。
比如:
public ChatAnswerDTO ask(ChatRequest request) { User user = userService.getUser(request.getUserId()); DatasetDTO dataset = datasetService.matchDataset(request.getQuestion()); if (!permissionService.canAccess(user, dataset)) { throw new BizException("无数据集权限"); } MetricDTO metric = metricService.resolveMetric(request.getQuestion(), dataset); String prompt = promptBuilder.build(request.getQuestion(), dataset, metric); String sql = llmClient.generateSql(prompt); sqlValidator.validate(sql); List<Map<String, Object>> rows = warehouseClient.query(sql); ChartDTO chart = chartService.buildChart(request.getQuestion(), rows); String summary = llmClient.summarize(request.getQuestion(), rows); chatRecordMapper.insert(request, sql, rows, summary); auditService.record(user, dataset, sql); return new ChatAnswerDTO(summary, chart); }这段代码表面上是在回答一个问题,实际上混了很多职责:
用户入口处理 问答流程编排 指标和口径识别 数据集权限判断 Prompt 构造 LLM 调用 SQL 校验 数仓查询 图表生成 会话记录 审计记录真正的问题不是“代码长”,而是这些职责的变化来源不同。
权限规则变化,不应该影响 LLM 调用代码。
指标口径变化,不应该影响 Controller。
模型供应商变化,不应该影响 ChatBI 的领域规则。
数仓查询方式变化,不应该影响问答语义建模。
DDD 首先帮我们看清:这里哪些是 ChatBI 的业务概念。
在 ChatBI 里,真正重要的业务概念可能不是Request、Response、Mapper,而是:
Question:用户问题 Conversation:会话 Dataset:数据集 Metric:指标 Dimension:维度 SemanticModel:语义模型 QueryIntent:查询意图 SqlDraft:待校验 SQL Answer:回答 DataPermission:数据权限这些概念不是为了“显得领域化”,而是因为业务人员、数据产品、数据开发都会围绕它们沟通。
DDD 要问的是:
用户问题如何被理解? 指标口径由谁决定? 数据集选择规则是什么? 权限检查基于用户、数据集,还是字段级权限? SQL 生成前后分别要满足什么规则? 回答结果是否属于一次会话的一部分?这些问题的目标,是把 ChatBI 的核心业务规则从技术流程里分离出来。
COLA 接着处理代码安放问题。
它把代码按职责放到不同层:
Adapter:接收外部请求 App:编排一次 ChatBI 问答用例 Domain:表达 ChatBI 的核心业务概念和规则 Infrastructure:适配 LLM、数仓、向量库、权限系统、审计系统 Client:定义对外命令、查询、DTO所以二者分工很清楚:
DDD 帮你识别 ChatBI 的业务模型。 COLA 帮你把入口、流程、规则、技术实现分开放。二、DDD 与 COLA 的关系:业务治理地图与代码货架
DDD 和 COLA 都会提到domain,但它们不是一回事。
DDD 是业务建模方法。它关心的是 ChatBI 世界里到底有哪些稳定概念,以及规则属于谁。
比如:
什么是指标? 什么是维度? 什么是语义模型? 什么叫用户有权限问这个问题? 什么叫 SQL 安全? 什么叫一个回答可信?COLA 是应用架构。它关心的是这些概念和规则写出来后,应该放在哪一层。
adapter 放 HTTP、RPC、消息入口 app 放 AskQuestionAppService 这类用例编排 domain 放 Question、Metric、Dataset、SemanticModel 等领域对象 infrastructure 放 LLM、数仓、向量库、审计、外部权限系统适配 client 放 AskQuestionCmd、ChatAnswerDTO 等契约所以,DDD 不等于 COLA。
你可以用 DDD 建模 ChatBI,但不用 COLA。
你也可以套了 COLA 目录,但 ChatBI 逻辑仍然贫血。
二者更准确的关系是:
DDD 是业务地图,COLA 是代码货架。
业务地图回答:
ChatBI 里有哪些关键业务概念? 一次问答的规则属于谁? 哪些概念属于语义层,哪些属于查询执行层? 权限、指标、SQL 安全的边界在哪里?代码货架回答:
入口放哪里? 用例编排放哪里? 业务规则放哪里? LLM 和数仓调用放哪里? 对外 DTO 放哪里?比如 DDD 识别出这些领域概念:
Question QueryIntent SemanticModel Metric Dataset DataPermission SqlDraft AnswerCOLA 可以这样承载:
domain/chat/Question.java domain/chat/Conversation.java domain/semantic/SemanticModel.java domain/semantic/Metric.java domain/dataset/Dataset.java domain/security/DataPermission.java domain/query/SqlDraft.java domain/answer/Answer.java一次问答用例放在:
app/AskQuestionAppService.java外部入口放在:
adapter/web/ChatController.java技术实现放在:
infrastructure/llm/LlmGatewayImpl.java infrastructure/warehouse/WarehouseGatewayImpl.java infrastructure/vector/VectorSearchGatewayImpl.java infrastructure/audit/ChatAuditGatewayImpl.java如果只是把ChatServiceImpl挪到domain/chat/ChatService,但里面仍然直接调 LLM、拼 Prompt、查数仓、写审计,那不是架构升级,只是换目录。
判断 COLA 有没有真正发挥作用,看三个问题:
ChatBI 的业务规则是不是进入了 Domain? LLM、数仓、向量库是不是隔离在 Infrastructure? App 层是不是只编排流程,而不是吞掉所有逻辑?把 DDD 和 COLA 合起来,可以形成一个顺序:
先用 DDD 问:这是 ChatBI 的业务概念吗? 再用 DDD 问:这条规则属于哪个业务对象? 然后用 COLA 问:这段代码应该放在哪一层? 最后用 COLA 问:依赖方向有没有反过来?这比直接照抄目录更重要。
三、ChatBI 案例:从胖 Service 到 DDD + COLA
我们用“用户提问生成数据分析回答”作为例子。
需求是:
用户输入自然语言问题。 系统识别问题意图。 选择可用数据集。 解析指标和维度。 检查数据权限。 生成 SQL。 校验 SQL 安全性。 执行查询。 生成图表和文字解释。 保存会话记录。如果写成一个胖 Service,短期最快。但这个场景的复杂度会持续增长,所以必须先区分业务规则和技术实现。
DDD 先看业务。
用户问“最近 30 天华东区 GMV 环比变化怎么样”,系统至少要理解:
最近 30 天:时间范围 华东区:筛选条件 GMV:指标 环比变化:分析意图 怎么样:回答形式这不是简单字符串处理,而是 ChatBI 的核心业务。
因此可以建模为:
Question:用户原始问题 QueryIntent:查询意图 Metric:指标 Dimension:维度 TimeRange:时间范围 Dataset:数据集 SemanticModel:语义模型 DataPermission:数据权限 SqlDraft:待校验 SQL Answer:最终回答其中,Metric不是数据库字段的别名。它应该表达业务口径。
比如 GMV:
指标名:GMV 业务含义:成交总额 计算口径:sum(pay_amount) 可用数据集:order_summary 默认时间字段:pay_time 权限约束:按组织和数据域过滤这就是 DDD 的价值:把“GMV”从一个 SQL 片段提升为业务概念。
接着,Domain 不应该直接依赖大模型、数仓或向量库。它定义自己需要的能力:
public interface IntentGateway { QueryIntent recognize(Question question, SemanticModel semanticModel); } public interface SqlGenerationGateway { SqlDraft generate(QueryPlan queryPlan); } public interface WarehouseGateway { QueryResult query(SqlDraft sql); }这些接口放在 Domain,是因为 Domain 要表达“我需要识别意图、生成 SQL、查询数据”。具体用哪个模型、哪个数仓、哪个 SDK,是 Infrastructure 的事。
COLA 之后可以这样组织:
client AskQuestionCmd ChatAnswerDTO adapter ChatController app AskQuestionAppService domain chat Question Conversation Answer semantic SemanticModel Metric Dimension TimeRange dataset Dataset DatasetSelector security DataPermission PermissionPolicy query QueryIntent QueryPlan SqlDraft SqlSafetyPolicy gateway IntentGateway SqlGenerationGateway WarehouseGateway AuditGateway infrastructure llm IntentGatewayImpl SqlGenerationGatewayImpl warehouse WarehouseGatewayImpl audit AuditGatewayImpl repository ConversationRepositoryImplApp 层只编排一次问答流程:
public ChatAnswerDTO ask(AskQuestionCmd cmd) { Question question = Question.of(cmd.getQuestion()); UserId userId = new UserId(cmd.getUserId()); SemanticModel model = semanticModelRepository.getDefault(); QueryIntent intent = intentGateway.recognize(question, model); Dataset dataset = datasetSelector.select(intent, model); permissionPolicy.check(userId, dataset, intent); QueryPlan plan = queryPlanner.plan(intent, dataset, model); SqlDraft sql = sqlGenerationGateway.generate(plan); sqlSafetyPolicy.check(sql); QueryResult result = warehouseGateway.query(sql); Answer answer = answerFactory.create(question, intent, result); conversationRepository.save(question, answer); auditGateway.record(userId, dataset, sql); return chatAnswerAssembler.toDTO(answer); }这段代码仍然能看出完整流程,但它不承载所有规则。
比如 SQL 安全规则可以放在 Domain:
public class SqlSafetyPolicy { public void check(SqlDraft sql) { if (!sql.isSelectOnly()) { throw new BizException("只允许查询语句"); } if (sql.hasUnsafeFunction()) { throw new BizException("SQL 包含高风险函数"); } if (!sql.hasLimit()) { throw new BizException("查询必须限制返回行数"); } } }指标口径也可以是领域对象:
public class Metric { private final MetricCode code; private final String expression; private final DatasetId datasetId; public boolean belongsTo(Dataset dataset) { return this.datasetId.equals(dataset.id()); } }LLM 调用留在 Infrastructure:
public class SqlGenerationGatewayImpl implements SqlGenerationGateway { public SqlDraft generate(QueryPlan plan) { String prompt = promptTemplate.render(plan); String sql = llmClient.generate(prompt); return SqlDraft.of(sql); } }这里的重点是:Prompt、模型 SDK、供应商参数没有进入 Domain。Domain 只知道自己需要一个SqlDraft。
这样拆完后,变化会被隔离。
模型从 OpenAI 换成通义千问,主要改:
SqlGenerationGatewayImpl IntentGatewayImplGMV 口径变化,主要改:
Metric SemanticModelSQL 安全规则变化,主要改:
SqlSafetyPolicy数仓从 Hive 换成 ClickHouse,主要改:
WarehouseGatewayImpl前端接口参数变化,主要改:
ChatController AskQuestionCmd这就是 DDD + COLA 的组合价值:业务规则回到业务模型,技术变化关进基础设施层。
四、回到架构思维:控制变化,而不是堆架构
学 DDD 和 COLA,最终不是为了背概念,而是训练判断力。
第一步是分类。
看到 ChatBI 里一段代码,先问它属于哪类职责:
它是在接收外部请求吗? 它是在编排一次问答流程吗? 它是在表达指标、权限、SQL 安全等业务规则吗? 它是在调用 LLM、数仓、向量库、审计系统吗?对应到 COLA:
Adapter:入口 App:流程 Domain:规则 Infrastructure:技术实现如果 Controller 里开始拼 Prompt,这是分类错误。
如果 Domain 里直接调用 LLM SDK,这是分类错误。
如果 AppService 里写满指标口径判断,这是分类错误。
如果 Infrastructure 决定“用户能不能看这个指标”,也是分类错误。
第二步是建模。
ChatBI 里最容易混淆的是:把技术对象当业务对象。
ChatRequest不是领域模型。SqlDTO不是领域模型。TableDO不是领域模型。PromptTemplate也不一定是领域模型。
真正值得建模的,是那些稳定的业务概念:
指标 维度 数据集 语义模型 查询意图 数据权限 SQL 安全策略 会话 回答判断标准很简单:
业务人员会不会说这个词? 数据产品会不会维护它? 它有没有规则? 它是否会因为业务变化而变化?如果答案是肯定的,它才值得进入 Domain。
第三步是划边界。
ChatBI 的变化来源很多,边界不清就会迅速失控。
入口变化挡在 Adapter。 问答流程变化挡在 App。 指标、权限、语义、SQL 安全规则变化挡在 Domain。 LLM、数仓、向量库、审计系统变化挡在 Infrastructure。比如模型供应商变化,不应该影响指标口径。
数仓 SQL 方言变化,不应该影响用户问题建模。
权限系统接口变化,不应该影响Question和Metric。
前端展示变化,不应该影响 SQL 安全策略。
边界的意义不是让文件变多,而是让变化停在该停的位置。
第四步是保持克制。
不是所有 ChatBI 能力都要完整 DDD。
如果只是一个内部 Demo:用户输入问题,直接拼 Prompt,生成 SQL,查库返回结果,用简单结构就够了。
但如果系统已经出现这些特征,就值得引入 DDD + COLA:
指标口径经常变化 权限规则复杂 数据集很多 多模型、多数仓并存 SQL 安全要求高 问答链路需要审计 多个团队共同维护语义层反过来,如果只是简单报表查询、一次性验证、短期 PoC,就不要硬造聚合、领域事件和复杂网关。
架构不是越重越好。成熟的判断是:
复杂度在哪里,就在哪里建模。 变化从哪里来,就在哪里设边界。 没有复杂度,就不要制造复杂度。结语
ChatBI 不是简单的“调用大模型生成 SQL”。真正难的是把自然语言、指标口径、语义模型、数据权限、SQL 安全和查询执行组织成一个可演进的系统。
DDD 和 COLA 合在一起,给出了一条清晰路径:
用 DDD 识别 ChatBI 的业务概念。 用 DDD 判断规则属于谁。 用 COLA 安放入口、流程、规则和技术实现。 用 COLA 控制依赖方向。一句话概括:
用 DDD 看清 ChatBI 的业务,用 COLA 放好 ChatBI 的代码。
好的架构不是目录漂亮,也不是概念堆满,而是变化发生时,你知道该改哪里,也知道哪里不该被影响。