Skip to content

选型决策树:RAG、微调、长上下文、Agent 怎么选 ​

1. 本节产出 ​

一棵能直接用的决策树:拿到一个业务需求,5 分钟内判断该走 RAG、微调、长上下文、Text-to-SQL 还是 Agent,并输出一份可写进方案文档的选型结论表(含成本量级估算)。

2. 前置依赖 ​

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高(编排 + 可靠性)高(多轮调用)改工具,天级月级
微调高(数据 + 训练 + 评测)低重新训练,天级月级

两个反直觉的结论:

  1. 微调和长上下文看起来「简单」,实际总成本高——前者贵在一次性投入和更新,后者贵在每次调用的 Token。
  2. 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. 生产避坑 ​

  1. 不要把需求当成一个整体来选技术。这是本节最核心的建议。真实需求几乎都是复合的,拆开后每个子任务往往有更合适、更便宜的解法。选型错误的代价不是慢一点,是整个方向错。
  2. 微调是最后选项,不是首选。它的三个隐性成本常被低估:训练数据要人工标注、评测标准难定、知识更新要重训。在 prompt 工程和 RAG 都没做过充分尝试之前上微调,几乎都是浪费。
  3. 长上下文方案的成本会随用量线性增长,且容易被忽略。单次看不出来,日活几千之后账单会突然变得很难看。做法是:上线前用预估 QPS 算一个季度成本,跟 RAG 方案对比后再定。

8. 延伸与锚点 ​

  • 思考题:如果路由判断错了(本该查库却走了 RAG),用户会看到什么?怎么让这个错误不致命?(提示:下游统一拒答 + 引导——答案在 02-11)
  • 代码锚点:git checkout ch02-02-decision-tree
  • 下一课时:02-03 文档解析
  • 对应课件:L02-02 选型决策树