Appearance
选型决策树:RAG、微调、长上下文、Agent 怎么选
1. 本节产出
一棵能直接用的决策树:拿到一个业务需求,5 分钟内判断该走 RAG、微调、长上下文、Text-to-SQL 还是 Agent,并输出一份可写进方案文档的选型结论表(含成本量级估算)。
2. 前置依赖
- 02-01 RAG 全景:已理解完整链路
- 00-04 成本速算:会估算 Token 成本
3. 为什么需要一棵显式的决策树
「用 RAG 还是微调」这个问题,团队里通常会有三种答案,且都觉得自己对:
| 角色 | 倾向 | 理由 |
|---|---|---|
| 算法出身 | 微调 | 「微调才是真正的模型能力」 |
| 工程出身 | RAG | 「微调要训练,太重了」 |
| 产品出身 | 都能做吧 | 「别人家 AI 不是什么都会吗」 |
争论的根源是没把「需求」拆开。一个需求里往往同时包含四种不同的子任务:
「帮我分析上季度华东区销售额下滑的原因,并生成汇报材料」
├─ 查数据 → Text-to-SQL(不是 RAG)
├─ 查制度文档 → RAG
├─ 分析推理 → 模型能力(prompt + 推理模型)
└─ 按模板输出 → 结构化输出 / 模板(不是微调)拆开之后发现:没有一种是微调。这就是决策树的价值——它把「选技术」变成「拆任务」。
4. 核心原理
4.1 五选一决策树
需求进来
│
├─ 需要实时/准实时数据吗?(库存、订单、行情)
│ └─ 是 → 工具调用 / Text-to-SQL,不用 RAG
│
├─ 答案在文档里,且文档会变吗?
│ ├─ 是,且需要溯源 → RAG(主力方案)
│ └─ 是,但文档少且固定 → 长上下文全塞(更简单)
│
├─ 是要改变模型的表达风格/输出格式吗?
│ └─ 是 → 先试 prompt + few-shot;仍不行再考虑微调
│
├─ 需要多步决策、调用多个系统吗?
│ └─ 是 → Agent(03 篇)
│
└─ 是要模型掌握大量领域术语与隐性知识吗?
└─ 是 → 微调(成本高,最后考虑)4.2 成本量级对比(同一需求,粗估)
| 方案 | 一次性投入 | 每次调用成本 | 更新知识成本 | 上线周期 |
|---|---|---|---|---|
| Prompt + few-shot | 极低 | 中(长 prompt) | 改文本,分钟级 | 天级 |
| 长上下文全塞 | 无 | 高(输入线性增长) | 改文本,分钟级 | 天级 |
| RAG | 中(建管道 + 评测集) | 中 | 增量灌库,分钟级 | 周级 |
| Text-to-SQL | 中(表结构 + 校验) | 低 | 改表结构,随库更新 | 周级 |
| Agent | 高(编排 + 可靠性) | 高(多轮调用) | 改工具,天级 | 月级 |
| 微调 | 高(数据 + 训练 + 评测) | 低 | 重新训练,天级 | 月级 |
两个反直觉的结论:
- 微调和长上下文看起来「简单」,实际总成本高——前者贵在一次性投入和更新,后者贵在每次调用的 Token。
- RAG 的「中」等投入主要花在评测集上,不是花在管道上。没有评测集的 RAG 等于闭眼开车。
4.3 组合策略:一个真实的企业方案
| 子任务 | 技术 | 理由 |
|---|---|---|
| 意图识别 | 轻量模型 + 分类 prompt | 输出只有几个字,省钱 |
| 查业务数据 | Text-to-SQL | 结构化,RAG 答不准数字 |
| 查制度文档 | RAG + Rerank | 需要溯源 |
| 复杂推理 | 旗舰模型 | 一步错满盘皆输 |
| 输出格式 | 结构化输出 | 进下游系统 |
| 多步流程 | Agent + 审批 | 涉及写操作 |
这才是真实的项目形态:不是一个技术打天下,而是按子任务分派。这套分派逻辑在 04-02 模型网关里会做成可配置路由。
5. 代码走查
5.1 用分类器做意图路由
java
// src/main/java/com/example/rag/route/IntentRouter.java
@Service
public class IntentRouter {
private final ChatClient classifier; // 轻量模型即可
public Route decide(String question) {
BeanOutputConverter<Intent> conv = new BeanOutputConverter<>(Intent.class);
String raw = classifier.prompt()
.user(u -> u.text("""
判断下面问题属于哪一类,只输出 JSON。
{format}
问题:{q}
""")
.param("format", conv.getFormat())
.param("q", question))
.options(OpenAiChatOptions.builder()
.temperature(0.0) // 分类必须稳定
.maxTokens(50)
.build())
.call().content();
Intent intent = conv.convert(raw);
return switch (intent.type()) {
case DOCUMENT -> Route.RAG; // 查制度文档
case DATA -> Route.SQL; // 查业务数据
case ACTION -> Route.AGENT; // 要执行动作
case CHITCHAT -> Route.DIRECT; // 闲聊,直接答
};
}
public record Intent(Type type, double confidence) {
public enum Type { DOCUMENT, DATA, ACTION, CHITCHAT }
}
}用轻量模型做分类是本节的省钱要点:分类任务输出只有几个 Token,用旗舰模型纯属浪费。这一层的成本通常是主流程的 1/20。
5.2 路由分发
java
// src/main/java/com/example/rag/route/QueryFacade.java
@Service
public class QueryFacade {
public Answer answer(String question, String tenantId) {
Route route = router.decide(question);
log.info("route={} q={}", route, question); // 路由必须可观测
return switch (route) {
case RAG -> ragService.ask(question, tenantId);
case SQL -> sqlService.query(question, tenantId);
case AGENT -> agentService.run(question, tenantId);
case DIRECT -> directService.chat(question);
};
}
}提示:路由结果必须打日志。上线后你会发现大量「本该走 SQL 的问题走了 RAG」,这些日志就是优化依据。没有日志的路由等于没有路由。
5.3 长上下文 vs RAG 的成本临界点
java
// 一个粗略的临界点计算
long docTokens = TokenEstimator.estimate(wholeDocument);
long ragTokens = systemTokens + topK * avgChunkTokens + questionTokens;
// 当 docTokens < ragTokens * 2 时,全塞更划算且效果更好
// (乘 2 是因为全塞方案实现更简单、无切分损失)经验临界点:文档总量在 8K~16K Token 以内时,全塞优于 RAG。超过这个量级,检索带来的成本节约开始超过切分带来的语义损失。这个数字会随模型降价而右移,但判断方法不变。
6. 跑起来
bash
git checkout ch02-02-decision-tree
mvn spring-boot:run用一组预置问题验证路由:
bash
# 1. 文档类 → 应路由到 RAG
curl -X POST http://localhost:8080/api/ask -d '{"q":"差旅报销标准是多少"}'
# 期望日志:route=RAG
# 2. 数据类 → 应路由到 SQL
curl -X POST http://localhost:8080/api/ask -d '{"q":"上季度华东区销售额是多少"}'
# 期望日志:route=SQL
# 3. 动作类 → 应路由到 AGENT
curl -X POST http://localhost:8080/api/ask -d '{"q":"帮我把订单 SO123 改成已发货"}'
# 期望日志:route=AGENT
# 4. 闲聊 → 直接答
curl -X POST http://localhost:8080/api/ask -d '{"q":"你好"}'
# 期望日志:route=DIRECT| 检查项 | 通过标准 |
|---|---|
| 四类路由 | 各跑 3 个样例,准确率 ≥ 8/12 |
| 分类稳定性 | 同一问题连跑 5 次结果一致 |
| 日志完整 | 每条请求都有 route 记录 |
| 成本对比 | 分类调用的 Token 数远小于主流程 |
准确率不要求 100%:意图路由错了会走错分支,但只要下游有兜底(比如 RAG 答不出时提示换问法),用户体验不会崩。追求 100% 分类准确率是过度设计。
7. 生产避坑
- 不要把需求当成一个整体来选技术。这是本节最核心的建议。真实需求几乎都是复合的,拆开后每个子任务往往有更合适、更便宜的解法。选型错误的代价不是慢一点,是整个方向错。
- 微调是最后选项,不是首选。它的三个隐性成本常被低估:训练数据要人工标注、评测标准难定、知识更新要重训。在 prompt 工程和 RAG 都没做过充分尝试之前上微调,几乎都是浪费。
- 长上下文方案的成本会随用量线性增长,且容易被忽略。单次看不出来,日活几千之后账单会突然变得很难看。做法是:上线前用预估 QPS 算一个季度成本,跟 RAG 方案对比后再定。
8. 延伸与锚点
- 思考题:如果路由判断错了(本该查库却走了 RAG),用户会看到什么?怎么让这个错误不致命?(提示:下游统一拒答 + 引导——答案在 02-11)
- 代码锚点:
git checkout ch02-02-decision-tree - 下一课时:02-03 文档解析
- 对应课件:L02-02 选型决策树