做RAG开发时间久了,你会发现一个很有意思的悖论:检索器越强,用户问的问题就越刁钻。我刚上手那会儿,也以为只要把文档切块、向量化、召回topK,再加一个会总结的LLM就万事大吉。结果一上真实场景就露馅——用户问的不是“报销标准是多少”这种单点问题,而是“发票丢了的情况下,我还按普通报销流程走吗,补什么材料,找谁签字”这种一串儿的复合问题。传统RAG拿到这个query,通常只干一件事:整句话向量化,去数据库里找语义相近的片段。运气好能捞到关于“发票丢失”的一段,但报销流程、签字审批、补材料这些零碎信息全丢了,最后生成的答案又空又没依据。
后来我把系统改造成两层:第一层叫“分诊台”,负责判断问题到底是什么性质、需要几步、走哪条路;第二层叫“拆题术”,负责把复杂问题拆成若干个能单独检索的小问题,再合并结果。这两个改动让我的RAG项目从“搜索引擎套壳”变成了一个能真正“判断和规划”的知识库Agent。这篇文章就是我最近实践的完整思路、实现代码和踩坑记录,适合正在做RAG问答、做AI应用,或者想用Ollama搭本地知识库的朋友。
1. 为什么RAG需要“分诊台”和“拆题术”
1.1 单次检索的天然瓶颈:把“复杂问题”当成“一个向量”
先理清最底层的逻辑。RAG的标准流程不复杂:用户问题进来,转成一个向量,去向量数据库里做相似度检索,拿回topK个文本片段,再丢给大模型生成答案。这个流程对“事实型单点问题”很有效,因为用户想知道的东西大概率集中在一个段落里,向量相似度能直接命中。
但问题一旦变成多跳、对比、带条件判断的形式,单向量检索就开始露怯。原因不复杂:一个长问题被Embedding模型压成一个向量时,语义信息已经被“平均”掉了。比如问题“A部门报销和B部门报销在差旅标准上的区别”,这个query里同时包含“A部门”“B部门”“差旅标准”“区别”四组信息,向量化之后就像把四张照片叠在一起曝光,结果哪张都看不清。向量库返回的片段可能只覆盖“A部门差旅标准”,B部门相关内容根本排不上号。
本地RAG场景这个问题更明显。本地知识库通常体量不大、文档来源杂,很多段落还带有表格、图片、页眉页脚,切块之后语义更碎。用户问“哪些合同的付款节点快到了,如果逾期需要走什么流程”,这个问题的答案可能分布在合同清单、付款条款、逾期处理三个不同文件里。一次检索只能捞到其中一部分,模型只好在“信息不足”的情况下硬着头皮编,这是很多RAG项目“一本正经胡说八道”的根源。
1.2 分诊和拆题不是玄学,是给检索流程装上“红绿灯”
我一开始也觉得,复杂问题无非是“多检索几次”,没必要搞什么分诊拆题。后来实测发现,盲目多次检索会把噪音放大:每次检索都拿整句,召回的重复片段多,互相矛盾的信息也多,最终答案质量反而下降。
真正的解法,是把“回答问题”这件事从一步拆成三步。第一步是分诊,像医院门口的护士,先判断病人是内科还是外科,是轻症还是重症;对应到系统里,就是判断用户问题属于闲聊、单点事实、多跳推理、还是需要查表格算数字。第二步是拆题,把复杂问题拆成多个相互独立、能被检索命中、也能被后续模型回答的子问题。第三步才是执行检索和生成答案。
这个思路的成本很低,但收益非常大。低在哪里?分诊和拆题本质上是让LLM做“输出文本结构”的工作,不需要额外训练,也不需要引入复杂框架,一个良好的Prompt加一个稳定的模型就能跑。收益高在哪里?我自己的项目里,长尾复杂问题的准确率大概提升了三成以上,不是模型变聪明了,而是检索喂给它的材料变准了。接下来我把这两层的设计细节逐一讲透,并给出一套可以直接抄的本地实现。
2. 分诊台:先判断,再动手
2.1 分诊层要产出什么
很多人做意图分类,喜欢把它做成一个单选分类标签,比如“闲聊”“问答”“工具调用”,但这对RAG来说远远不够。我这边实践下来的结果,分诊层最好一次性输出一组结构化信息,至少包括:
intent_type:当前问题的类型,例如事实问答、多跳推理、对比分析、操作流程指导、闲聊、信息不足。complexity:问题的复杂度评分,建议从0到1,用来决定是否需要进入拆题流程。is_multi_hop:是否属于多跳问题,也就是答案需要跨多个文档片段才能拼出来。sub_questions:如果判断需要拆题,预生成的一组子问题,后续直接拿去检索。route_target:路由目标,包括向量检索、全文检索、对话历史、外部工具、统计计算。needs_image:判断答案是否依赖知识库里的图片或表格。context_dependency:是否依赖上一轮对话,如果依赖,先让系统拼接对话记忆再分发。
为什么要让分诊层直接生成sub_questions?从工程角度讲,省一次交互。分诊和拆题同属“动手前规划”的环节,放在一个LLM调用里完成,既降低延迟,也避免两次调用之间上下文不一致。实测用Qwen和Llama系模型都能稳定输出,关键在于把输出格式用JSON约束死,并且把候选意图的说明写清楚。
2.2 用结构化输出实现分诊层
我这里给出一个可以直接跑的Prompt模板,兼容OpenAI接口和Ollama本地接口。核心技巧是两句话:第一,告诉模型“你不是在回答用户,你是在给检索系统做路由判断”;第二,明确要求“只输出JSON,不要输出任何解释”。
import json TRIAGE_SYSTEM_PROMPT = """你是一个RAG系统的分诊模块。你的唯一任务是分析用户问题,并输出JSON结构,用于后续检索规划。 可选的intent_type包括: - fact: 单点事实问答,只需要检索一个片段即可回答 - multi_hop: 多跳问题,答案散落在多个文档片段中,必须拆解为子问题 - comparison: 对比型问题,需要同时检索多个实体各自的信息 - procedure: 流程型问题,需要按步骤组织多个检索结果 - chat: 闲聊、打招呼,不需要检索知识库 - tool: 需要调用外部计算工具,例如日期计算、算术运算 complexity取值0到1,0表示一句现成答案就能回复,1表示必须经过完整检索编排。 route_target只能是vector、hybrid、history、tool、none其中之一。 needs_image表示答案是否依赖知识库中的图片或表格内容。 请严格输出如下JSON格式,不要输出任何其他内容: {"intent_type": "...", "complexity": 0.0, "is_multi_hop": true/false, "sub_questions": ["...", "..."], "route_target": "...", "needs_image": true/false, "context_dependency": true/false} """ def triage(question: str, llm) -> dict: response = llm.chat( messages=[ {"role": "system", "content": TRIAGE_SYSTEM_PROMPT}, {"role": "user", "content": f"用户问题:{question}"} ], temperature=0, response_format={"type": "json_object"} # 本地Ollama若不支持JSON mode可删掉 ) try: return json.loads(response) except json.JSONDecodeError: # 兜底:解析失败就回到普通向量检索 return { "intent_type": "fact", "complexity": 0.2, "is_multi_hop": False, "sub_questions": [question], "route_target": "vector", "needs_image": False, "context_dependency": False }这里有个非常容易踩的坑:本地模型没有稳定支持JSON mode的话,模型可能在JSON前后输出大段废话,导致json.loads失败。我的处理办法是,先用正则抽取花括号部分再解析,同时在Prompt里把“不要输出任何其他内容”加粗强调。温度一定要调低,最好设成0,否则同一个问题可能一会儿拆题一会儿不拆题。
2.3 路由决策表:把分诊结果变成可执行动作
分诊层输出结果之后,系统不该一股脑全走向量检索。我维护了一个很朴素的决策表,规则简单但很有效:
| 分诊结果 | 典型问法 | 实际路由动作 | 是否需要拆题 |
|---|---|---|---|
| fact | “报销上限是多少” | 向量检索topK 3-5,直接生成 | 不需要 |
| multi_hop | “发票丢了,还要走什么补充手续” | 按sub_questions逐次检索,合并证据 | 需要 |
| comparison | “A和B部门的差旅标准有什么区别” | 每个子问题单独检索A、B,最后对比 | 需要 |
| procedure | “新员工入职要办哪些手续” | 按步骤检索多个片段,再生成流程说明 | 需要 |
| chat | “你好,谢谢” | 不进知识库,直接模型闲聊回复 | 不需要 |
| tool | “今年第三季度是哪几个月”“20天前是几号” | 调用日期计算工具,不检索知识库 | 不需要 |
这张表看起来简单,但它解决了一个非常实际的浪费问题。很多RAG系统不管用户问什么都拉一遍向量库,又慢又容易污染上下文。有了分诊,至少有三种情况可以不碰向量库:闲聊、纯计算、以及“知识库明显没有答案”的问题。我甚至会在分诊层加一个confidence字段,低于0.5时直接告诉用户“这个问题知识库里可能没有资料”,宁可承认不知道,也不要硬编。
2.4 分诊层的两个不稳定因素
分诊层第一个不稳定因素是意图类型过细。我最早定义了十几种意图,结果模型频繁混淆“对比”和“多跳”。后来合并成六类,准确率明显上升。给新手一个建议:意图类型控制在5到8个之间,宁可让模型归到宽泛类型,也不要为了“看起来智能”而细分到难以区分。
第二个不稳定因素是拆题过多。有的问题本来一句话就能回答,分诊模型却硬拆成五个子问题,检索次数翻五倍,答案反而啰嗦。解决办法是给sub_questions加数量上限,一般最多三个子问题。如果超过三个,就把原始问题交给一个“Fallback检索流程”,让用户简化表达之后再问。从产品体验上说,这比让模型硬答更有诚意。
3. 拆题术:把大问题切成检索友好的小问题
3.1 拆题的核心逻辑:让每个子问题都“指得准”
拆题的目标不是“显得有逻辑”,而是解决检索粒度错配的问题。文档切块的时候,我们通常按500到800个字符去切,切出来的块天然是“局部视角”。用户的一个大问题可能横跨多个块,甚至跨越多个文档,直接拿大问题去检索,就像在一个城市地图上搜一条街道的店名却输入了整个城市的位置信息,误差被无限放大。
拆题的本质,是让每个子问题都收敛到单一事实、单一实体或单一关系上。举个例子,用户问“合同付款逾期该怎么处理”,我会让分诊层拆成“合同付款标准是什么”“逾期处理流程是什么”“逾期是否需要支付违约金”三个方向。每一个子问题都指向一个明确的文档片段,向量检索的命中率会高很多。
拆题还有一个隐藏价值:它可以做事实交叉验证。同一件事,从不同角度检索回来的材料可能存在矛盾。有了子问题的检索结果,最终生成阶段就能让模型逐个比对,而不是把所有信息混在一起“和稀泥”。这对金融、法律、医疗这类对准确性要求高的本地知识库特别重要。
3.2 子问题生成与顺序检索
拆题可以放在分诊层一起做,也可以单独拆一个模块。如果问题本身已经被判断为multi_hop或comparison,我建议再调用一次专门的拆题Prompt,让模型输出与原始问题严格对应的子问题列表,每个子问题都带一个检索关键词字段,这个字段用来做混合检索的关键词补充。
这里给出一段拆题Prompt的核心结构:
SPLIT_PROMPT = """请把下面的用户问题拆解为最多3个子问题。 要求: 1. 每个子问题必须可以在知识库中独立检索到答案; 2. 子问题之间不能有语义重叠; 3. 子问题的总和要覆盖原始问题的所有信息点; 4. 为每个子问题生成一组检索关键词,关键词尽量使用原文中的专有名词。 输出格式: {"sub_questions": [{"question": "...", "keywords": ["...", "..."]}]} 用户问题:{question}"""拆完之后,执行检索时有一个顺序问题:如果子问题之间存在依赖关系,最好按顺序跑,前一个子问题的检索结果可以作为后一个子问题的上下文。比如“报销金额超过5000需要谁审批”,必须先检索“报销金额分层标准”,得到“超过5000”属于哪一层,再去检索“该层级对应审批人”。如果并行检索,第二个子问题缺少前一个结果的条件,很容易找错材料。
顺序检索在代码上很好处理:维护一个retrieved_context列表,跑完一个子问题就把结果追加进去,再拼接到下一个子问题的检索query里。代价是速度慢一点,但准确率提升明显,值得的。
3.3 合并结果与交叉验证
子问题检索完之后,手里会有一堆文本片段。直接全塞给模型不行,会撑爆上下文,也会让模型迷失重点。我通常会做一个轻量“证据裁剪”,规则如下:
- 把检索片段按来源文档分组,保证最终答案不被单一文档带偏。
- 同一来源里取与子问题关键词重合度最高的句子,去掉开头和结尾的无意义段落。
- 如果两个片段内容完全矛盾,不要在这里做裁决,把矛盾原样提交给最终生成模型,让它尝试仲裁;仲裁不了就直接告诉用户“知识库中存在不一致表述”。
裁剪后的证据集最后会拼成一个Markdown格式的“证据列表”,再连同原始问题和所有子问题一起交给生成模型,并附加一句要求:“答案必须基于证据列表,不要引用证据之外的任何知识。”
def build_final_prompt(question, sub_questions, evidence_blocks): evidence_text = "\n\n".join( f"[来源{i}] {block}" for i, block in enumerate(evidence_blocks) ) return f"""请基于下面的证据回答用户问题。你需要先分析证据,再组织答案。 如果证据不足,请直接说“知识库中未找到足够信息”。 用户问题:{question} 拆解出的子问题: 1. {sub_questions[0]} 2. {sub_questions[1]} 3. {sub_questions[2]} 证据列表: {evidence_text}"""3.4 从“拆题”到“本体增强”与本地检索工具
拆题解决的是“问题划得准不准”,但有些问题就算划准了,检索层也未必接得住。这时候有几个辅助手段非常有用。
第一个手段是引入本体(Ontology)约束。简单说,本体就是知识库里的实体关系图,比如“合同”和“付款节点”之间的关系,“员工”和“部门”之间的关系。如果项目里有本体层,拆题之后可以先在实体关系图上做一轮路径检索,把可能相关的实体和关系提前捞出来,再带着这些实体去向量库里找文本。这比单纯靠向量相似度可靠,因为向量库管不住逻辑关系。比如“哪些合同付款节点快到期”这个问题,有本体的情况下可以直接查“合同——付款日期——今天”这三元组,没本体就只能靠模型猜。
第二个手段是本地文本拆解工具。很多人在预处理阶段偷懒,PDF直接整页切块,结果块里混着页眉、页码、表格碎片。我建议先把PDF、Word转成结构化的Markdown或JSON,按标题、段落、表格分别处理,表格单独转成Markdown表格存为独立块。本地常用做法是用markitdown这类工具转格式,再按文档标题层级切块。切块时保留上一层标题作为该块的“上下文标签”,检索命中时可以一并返回,方便生成模型理解当前片段在原文中的位置。
顺带回答一个我经常被问到的问题:RAG知识库能存储图片吗?直接存图片向量不是不行,但前提是用CLIP这类多模态模型,否则普通文本Embedding模型根本处理不了图片。如果不想上多模态,更务实的路线是图片先OCR转文字,或者写一句图片描述存入知识库。表格同理,建议转成Markdown或CSV文本块,不要直接存原图。
4. 实操:搭一个会判断和规划的本地RAG
4.1 技术栈选型:Ollama加本地Embedding的零基础方案
先说明我的本地环境选型。模型用Ollama跑,一个LLM模型负责分诊、拆题和最终生成,一个Embedding模型负责文本向量化,向量库用Chroma。这套组合的好处是零成本、可复现、不依赖云端,非常适合先把流程跑通。
具体组件如下:
| 组件 | 推荐选择 | 说明 |
|---|---|---|
| LLM | qwen2.5:7b或llama3.1:8b | 7B/8B本地可跑,分诊拆题足够稳定 |
| Embedding | bge-m3或nomic-embed-text | 中文场景优先bge-m3 |
| 向量数据库 | Chroma或FAISS | 小规模知识库用Chroma更省事 |
| 文档解析 | markitdown加自定义清洗脚本 | 把PDF/Word转成Markdown |
| 开发语言 | Python | 生态最全,调试方便 |
如果你用的是Java技术栈,LangChain4j里也有一个Easy RAG模块,思路完全一致:先做文档切块入库,再做检索问答。只是它在分诊和拆题上往往需要自己补Prompt,不像Python这边可以快速写自定义胶水代码。我两边都试过,结论是核心逻辑跟语言无关,先把本文这套流程理解透,用什么框架都能落地。
4.2 文档处理与知识库构建
知识库构建这一步决定上限。很多RAG项目效果差,不是模型问题,是文档进库之前就被处理坏了。我每次新增文档类型,都会先做一轮人工抽检:转出来的Markdown里有没有乱码?表格拆没拆散?页眉页脚有没有混进正文?只有抽检通过,才允许进入批量入库流程。
一个典型的构建脚本要点如下:
import os from chromadb import PersistentClient TEXT_DIR = "./docs" CHUNK_SIZE = 600 CHUNK_OVERLAP = 100 def split_text(text: str, size=CHUNK_SIZE, overlap=CHUNK_OVERLAP): chunks = [] start = 0 while start < len(text): end = min(start + size, len(text)) chunks.append(text[start:end]) if end == len(text): break start = end - overlap return chunks # 假设 docs 已通过 markitdown 转为 markdown 文件 for file_name in os.listdir(TEXT_DIR): if not file_name.endswith(".md"): continue with open(os.path.join(TEXT_DIR, file_name), encoding="utf-8") as f: content = f.read() for chunk in split_text(content): # 使用 embedding 模型生成向量,存入 Chroma 时把文件名和 chunk 一并保存 pass关于分块大小,我在这里给一个经验值:日常文本500到800字符,代码或表格类内容适当减小到300左右,太大会稀释语义,太小则会切断完整句子。重叠一般设100到150,保证跨块的关键句不会被拦腰截断。向量检索的topK一般取4到6,不是越多越好,多了会把无关噪声带进来。
4.3 完整主流程:分诊、拆题、检索、生成一体化
下面是我实际项目里的主流程简化版,把分诊、拆题、检索、生成串在一起。代码里省略了向量库细节,但结构是完整的。
def mini_rag_pipeline(question: str, memory: list, llm, vector_db): # 第一步:拼接对话历史 if memory: question_with_context = f"结合对话上下文理解:{question}" else: question_with_context = question # 第二步:分诊 triage_result = triage(question_with_context, llm) # 第三步:按路由执行 if triage_result["route_target"] == "chat": return llm.chat(question_with_context) if triage_result["route_target"] == "tool": return call_tool(question_with_context) # 第四步:复杂问题走拆题流程 if triage_result["is_multi_hop"] and triage_result["sub_questions"]: sub_questions = triage_result["sub_questions"] else: sub_questions = [question_with_context] # 第五步:逐个检索并收集证据 evidence_blocks = [] for sub_q in sub_questions: results = vector_db.query(sub_q, top_k=5) for r in results: evidence_blocks.append(r["text"]) # 第六步:合并证据生成最终答案 if not evidence_blocks: return "知识库中未找到与该问题相关的资料。" final_prompt = build_final_prompt(question_with_context, sub_questions, evidence_blocks) return llm.chat(final_prompt)这里有一个特别需要注意的设计:分诊层判断为fact时,也能走同一个检索流程,只是子问题列表就是原始问题本身,不需要额外拆题。这样代码路径统一,排查bug时省很多事。
在本地Ollama环境里,llm.chat接口可以直接用HTTP调用,也可以用langchain-ollama封装。如果发现7B模型在分诊层输出JSON偶尔报错,我的建议是先升级到14B模型,成本高一点,但分诊稳定性提升显著。另一个技巧是把分诊Prompt里每个意图都加一个例句,模型理解成本会大幅下降。
4.4 调优重点:阈值、分块、上下文长度
流程跑通之后,真正的调试工作才开始。我最常调的有三个点:
第一是相似度阈值。向量检索返回的相似度分数不是一个绝对可信指标,不同Embedding模型分布差异很大。我通常先跑200条测试question,统计“答对时相似度大概多少”和“答错时相似度大概多少”,找一个中间值作为min_score,低于这个值的检索结果直接丢弃。这个值不宜设太高,否则容易召回为空;也不宜设太低,否则噪声多。
第二是分块与检索的匹配。如果发现某个类型的问题总是命中同一个大块,却答不准,很可能是分块太大,信息密度不够。我一般会针对坏case单独检查原文,确认问题的答案究竟在哪个段落,然后调整分块策略。比如把“合同类”文档改成按条款拆块,而不是按字符硬切。
第三是上下文窗口。本地模型窗口通常有限,证据列表太长会挤掉指令内容。我控制最终Prompt里的证据总数不超过8条,每条不超过300字。如果检索命中了更多,就按“与子问题关键词重合次数”排序,保留最相关的几条。这个排序逻辑虽然简单,但比纯按相似度分数排序更稳定。
5. 常见问题与排查技巧实录
5.1 分诊判断不准怎么办
分诊不准的原因,排第一的是Prompt描述不清晰,其次是模型太小。如果你是本地7B模型,建议不要直接复刻网上的复杂Prompt,先跑一条“单类型识别”的极简Prompt:让模型只回答multi_hop还是fact,跑通60个例子的准确率,再加复杂字段。加字段要一个个加,每加一个字段跑一遍回归,看是否把已有准确率带崩。
还有一个技巧是给分诊模块做缓存。用户经常会用同义句问同一个问题,比如“发票丢了咋办”和“发票丢失了怎么处理”,分诊结果应该一致。我把分诊输入输出做成哈希缓存,命中缓存直接复用结果,省一次模型调用,也能避免模型偶尔抽风导致的随机路由。
5.2 子问题检索不到任何片段
这是拆题流程最常见的失败模式。原因通常不是子问题本身有问题,而是子问题的问法和知识库文本的表达方式相差太远。比如知识库原文写的是“补开发票”,子问题里问的是“重新获取凭证”,语义相近但用词不同,如果没有同义词扩展,向量检索分数会很低。
解决办法有两个。一个是在子问题里额外生成一组keywords字段,用关键词做BM25全文检索,再把全文检索结果和向量检索结果合并取并集。另一个是把分块时保留的标题信息拿来做“父文档召回”,也就是先把标题块加进去一起检索,用大片段确定文档范围后,再回到该文档内部定位具体段落。这两个方案都可以用,建议项目中同时做。
5.3 多个子答案互相矛盾
子问题独立检索,天然会带回不同来源的材料,而知识库本身可能就存在新旧版本不一致。遇到这种情况,不要把矛盾信息全部压给模型让它自行“脑补”。我在证据裁剪阶段会把互相矛盾的片段都保留,并标注来源文档和段落号,在生成Prompt中加一句“如果证据之间存在矛盾,请明确指出并给出你的判断依据,不要回避冲突”。
实测下来,模型在这种设定下会变得谨慎很多,至少不会强行缝合出一个貌似合理的答案。如果项目对准确性要求极高,还可以加一个“事后校验”环节:把最终答案里的关键事实拆成断言,再去知识库里各自检索一次,看是否有支持证据。这一步成本高,只建议高危场景用。
5.4 图片与表格在RAG里的处理
回到热词里反复出现的“RAG知识库能存储图片嘛”这个问题。答案是要分情况。普通文本Embedding模型处理不了图片,但图片在业务文档里往往有重要信息,比如流程图、流程图、证件扫描件。我的建议是:
- 如果图片是扫描件或截图,优先OCR成文字,存成文本块,检索时以文字方式召回;
- 如果图片是纯视觉信息,比如柱状图、折线图,可以先用多模态模型生成一段“图表摘要”,摘要入库,图片本身存为附件;
- 如果既没有OCR能力,也没有多模态模型,那就在入库阶段明确标记“该文档包含图片,但当前知识库不支持图片定位”,不要让检索层硬搜。
表格的处理则比图片更简单也更必要。原始表格一旦被切块成文本,行列关系基本就废了。我的做法是把表格统一转成Markdown,再把每张表格当作一个独立块,检索时如果query里出现“表格”“对比”“分布”这类词,就额外在表格块里做一次关键词搜索,效果比纯向量检索好很多。
5.5 用LangChain4j等框架接入时的注意点
框架类工具能帮我们省去很多基建功夫,但自带逻辑往往只覆盖“基础RAG”,不包含复杂的分诊和拆题。用LangChain4j的Easy RAG做原型很方便,一路跑通文档切块、向量化和检索问答;但等到接真实业务时,有两个坑一定会遇到。
第一个坑是切块策略。框架默认的分块参数未必适配你的文档。第二个坑是检索结果没有结构标签,不知道每个片段来自哪个文档、哪个章节。我的经验是,不管用什么框架,都要在入库时保留一段“元数据”,至少包含文档名、一级标题、二级标题、片段序号。这个元数据在证据裁剪、溯源展示、矛盾排查时都是刚需。
从工程实践上讲,框架只是工具,真正的竞争力还是在于你能否把“分诊台”和“拆题术”这两个模块设计得足够贴合业务。核心逻辑一定是可解释、可调试、可降级的,而不是黑盒地丢给一个Agent框架。
最后分享一个小技巧:做完分诊和拆题之后,把每一次调用的输入输出都落成日志,包括原始问题、分诊JSON、子问题列表、每个子问题命中的片段和相似度分数。这样系统跑一段时间后,你会发现所有badcase都有迹可循,甚至能从失败模式里反向总结出一套针对你自己知识库的检索规则。我个人实践下来的体会是,RAG项目越往后调,越依赖这种“可回溯的工程克制力”,而不是盲目堆模型大小。分诊台和拆题术这套设计,恰好能帮你把这种克制力沉淀进系统里。