你研究过大模型在开放域问答里的表现吗?如果做过 Agent 或客服机器人类项目,大概率会遇到一个场景:用户丢来一道数学题,模型不仅能算对,还写出了完整推导过程;但同一个模型换成一个“帮我比价两套云主机配置”的诉求时,它的回答就开始含糊其辞,甚至一本正经地编造价格。用户在后台回了一句:“哼,它只是碰巧蒙对而已。”这句话听起来像玩笑,但作为工程开发人员我们要意识到,用户其实点名了一个真实存在且影响系统可靠性的问题:模型不知道自己的能力边界,它只是在被动的“有什么答什么”,而不是“擅长什么答什么”。
如果一个 AI 系统能够识别“这个问题属于数学领域,我擅长”,以及“这个问题涉及实时比价,我的知识截止时间不支持,我不擅长”,那么它完全可以主动说“这次我先认输,下次我要选我擅长的任务再发挥”。这不是拟人化营销话术,这是任务路由(Task Routing)和模型能力边界感知的工程问题。本文会从模型输出不稳定的根因说起,讲清楚为什么它会“碰巧蒙对”,然后给出三种可落地的“擅长任务选择器”实现方案,包括关键词规则路由、Embedding 语义分类路由、以及基于自评估置信度的兜底路由,最终落到工程部署和排错建议。
1. 这句话背后的严肃工程问题
“碰巧蒙对”和“稳定答对”之间隔着一条很深的工程鸿沟。前者是模型输出的随机性带来的概率事件,后者是系统设计带来的确定结果。很多入门开发者会把模型当成稳定的计算器,认为同一个问题输入两次,模型就应该给出一样的正确答案。但大模型本质上是一个概率分布采样器,它的输出受到四个主要因素影响:训练数据覆盖范围、推理时的采样参数、上下文窗口的信息完整度、以及模型本身的参数量级。这四个因素组合起来,决定了它在不同任务上能力波动非常大。
更麻烦的是,很多业务系统在接入大模型时只接了一个模型,不管用户问的是数学、编程、法律、医疗还是实时交通,都往同一个上下文里塞。当模型遇到训练数据里出现次数多、格式规整的问题时,它表现不错,这就会被认为是“擅长”;一旦遇到冷门领域或需要最新信息的场景,它就进入“幻觉高发区”。这也是用户会觉得“你只是碰巧蒙对”的直接原因——因为下次再问相似但稍微变体的问题,答案可能就完全崩了。
所以这篇文章要解决的核心问题不是“如何把模型训练得更强”,而是“如何在工程上承认模型有短板,并让系统自动选择当前请求应该由哪个模型、哪个 Prompt 策略或哪个工具链路来回答”。换句话说,我们要给大模型加一个前置机构:先判断“这题是不是我擅长的”,再决定“要不要回答,以及派谁来回答”。这就是任务路由器的职责。
2. 基础概念与核心原理
2.1 模型能力边界:为什么模型有擅长和不擅长之分
大模型的能力边界,本质上是训练数据分布决定的。一个模型在训练阶段见过大量高质量的 Python 代码,它生成代码的能力就会很强;如果训练数据里数学推导语料占比少,或者数学题格式变化大,它的数学能力点就会稀疏,遇到没见过的题型就容易答错。
能力边界不只是一个理论概念,它会直接影响业务指标。比如一个智能客服系统,80% 的流量是退换货咨询,模型在这类问题上正确率达到 92%,剩下 20% 的流量是订单发票税务咨询,模型正确率只有 61%。如果系统不区分流量类型,整体正确率会被拉到 86% 左右;但如果按任务类型路由,把发票税务咨询交给一个擅长该领域的小模型或专用 Prompt 链路处理,整体正确率可能提升到接近 90%。
这个例子说明一个关键判断:给系统引入“擅长 / 不擅长”的判断,比单纯换一个大参数模型往往更有效,也更加省钱。
2.2 任务路由:把问题分发给最佳处理器
任务路由并不是新概念,传统微服务架构里的网关路由、消息队列里的消息分类都是类似思想。但在大模型应用场景里,任务路由的对象不是 HTTP 请求,而是“自然语言问题”。路由层需要做的是把用户输入映射到一个或多个可执行引擎上,这个引擎可以是一个大模型 API、一个本地小模型、一段固定 Prompt 的知识库查询流程,甚至是一个代码解释器。
路由的判断依据大致分三级:关键词匹配、语义相似度匹配、模型自评估匹配。低级方案速度快但泛化差,高级方案泛化好但开销大。实际系统往往会用多级结构,先用关键词过滤常见意图,再用语义分类做兜底,最后用自评估模块在发货前把关。这个多级思路,也会沿用到底下的代码示例中。
2.3 模型自评估:让模型告诉我们它有多少把握
“下次我要选我擅长的”这句话翻译成技术语言,就是让模型对自己的回答做一个置信度评估。目前比较可靠的方式不是直接问模型“你确定吗”,因为模型会倾向于给出虚高的信心;工程上更稳妥的方式是构造一个独立的裁判 Prompt,让裁判模型基于原始问题和候选答案给出 0 到 1 的置信度分数,或者做一次一致性校验:让模型用不同温度参数生成多次,再比较答案之间的语义一致性。
坦白说,自评估并不是银弹。强模型当裁判的效果通常好于弱模型,但当裁判模型和业务模型是同一个模型时,它会存在“自我偏好”偏差,也就是自己答什么看什么都顺眼。因此,自评估在工程上更适合作为“最后一道守门员”,不能作为唯一的路由依据。
2.4 三种主流路由方案对比
| 路由方案 | 实现成本 | 时延开销 | 泛化能力 | 适用场景 |
|---|---|---|---|---|
| 关键词规则路由 | 极低 | 无 | 弱,需要人工维护词表 | 意图范围固定、业务边界清晰 |
| Embedding 语义分类路由 | 低到中 | 一次向量化计算 | 较强,可适配近义表达 | 多数生产环境的工程首选 |
| 模型自评估置信度路由 | 中到高 | 额外一次模型调用 | 强,但受裁判模型影响 | 对错误答案容忍度低的场景 |
3. 环境准备与前置条件
以下示例使用 Python 3.9+ 环境,并调用 OpenAI 兼容接口来完成大模型推理和 Embedding 计算。为了让代码具备参考价值,本文统一使用兼容接口的客户端配置方式,你可以在环境变量中配置自己的 API Key、Base URL 和模型名。版本细节以实际使用的服务商为准,不同平台在接口路径上可能有差异,但整体思路通用。
准备步骤如下:
- 安装 OpenAI Python SDK 或同类兼容包。
- 配置环境变量,包括 API Key、Base URL。
- 准备一个本地 JSON 文件用于记录各引擎的描述信息和模型名。
- 准备一组用于测试路由效果的最小问题集。
安装命令:
pip install openai环境变量示例:
export OPENAI_API_KEY=your_api_key_here export OPENAI_BASE_URL=https://your-endpoint.example.com为了跑通示例,你需要有两种可用能力:
- 一个文本生成模型,用于最终回答用户问题,可以是通用大模型,也可以是几个不同领域的小模型。
- 一个 Embedding 模型,用于计算用户问题和引擎描述文本的向量相似度。
如果你的项目没有多个模型可用,也可以在同一个大模型上使用不同的 System Prompt 来模拟“不同引擎”,虽然效果不如真正的专用模型明显,但路由链路是完整成立的。
4. 核心流程拆解
完整的多级路由流程可以拆成三个阶段,分别是意图识别阶段、候选生成阶段、兜底校验阶段。下面逐个说明每个阶段做什么、为什么需要、以及常见误区。
4.1 意图识别阶段:先查规则表
意图识别阶段的核心目标是快。用户输入到达路由层后,先用关键词规则表做一次粗筛。比如出现“解方程”“求导”“积分”等词,就可以直接判给数学引擎;出现“Python”“Java 代码”“报错”等词,就可以判给代码引擎。
这个阶段的优点是零时延、零额外模型调用,缺点是关键词覆盖不全。同一意图的表达方式太多,比如“帮我算一下这个不定积分”和“这个式子怎么积分?”都指向数学,但关键词不完全一样。所以规则阶段只能做前置初筛,不能作为唯一判断。写规则时需要特别注意关键词的误命中,比如“积分”在电商场景里可能指“积分兑换”,这就需要在规则设计时增加上下文限定词。
4.2 候选生成阶段:语义相似度兜底
规则没命中时,进入语义分类阶段。把用户问题向量化,再与每个“引擎描述向量”做余弦相似度计算,选相似度最高的引擎作为候选。为了让匹配更稳定,每个引擎的描述文本写在配置文件里,启动时一次性向量化并缓存。
这个阶段解决了泛化问题,但引入了新问题:描述文本写得不好,相似度匹配就会偏。比如数学引擎的描述写的是“擅长解一元二次方程”,用户问的是“求导”,两者字面上看起来不相关,但语义上同属数学领域,描述文本需要覆盖该领域的常用说法,而不是写得太窄。建议每个引擎描述写到 50 到 100 字,列出该领域的典型任务词和边界。
4.3 兜底校验阶段:置信度自评估
找到候选引擎后,用它生成答案。生成完成后,用一个裁判 Prompt 对答案打分。如果置信度低于阈值,说明引擎在这个问题上并没有把握,触发二次路由:换一个引擎重试,或者直接降级为“明确承认不擅长”并返回人工提示。
这个阶段是“下次我要选我擅长的”这句话的工程落点。模型在低置信度时不应强行输出,而应返回一个结构化的不可靠标记。在实际系统中,这个标记会触发兜底策略,比如转接人工客服或者附上资料链接。兜底策略的设计直接影响用户体验,宁可让用户知道“这个问题我答不了”,也不能让用户信任一次错误回答。
5. 完整示例与代码实现
5.1 引擎配置
先准备一个引擎配置文件,遵循 JSON 格式。生产环境中,这个文件也可以替换为配置中心或数据库表。
{ "engines": { "math-engine": { "model": "your-math-model-name", "description": "擅长数学计算、方程求解、求导、积分、极限、概率统计和逻辑证明类问题。如果用户给出算式或数学概念说明,优先选择本引擎。", "system_prompt": "你是一位严谨的数学解题助手,步骤要完整,不确定时说明依据。" }, "code-engine": { "model": "your-code-model-name", "description": "擅长编程代码生成、代码调试、Bug 分析、算法设计、正则表达式和命令行操作类问题。如果问题提到编程语言或报错信息,优先选择本引擎。", "system_prompt": "你是一位资深程序员,请给出可以直接运行的代码和必要的解释。" }, "knowledge-engine": { "model": "your-knowledge-model-name", "description": "擅长常识问答、历史人文、科学知识、工作流程建议、文案写作和生活经验类问题。", "system_prompt": "你是一个知识面广泛且表达通顺的助手,回答尽量有依据,避免断言。" } } }5.2 规则路由实现
# 文件路径:router/rules_router.py ROUTING_RULES = [ { "engine": "math-engine", "keywords": ["解方程", "求导", "积分", "概率", "数学", "计算一下", "等于多少", "公式推导"] }, { "engine": "code-engine", "keywords": ["代码", "报错", "bug", "python", "java", "sql", "正则", "算法", "函数"] }, ] def match_rule(text: str): text_lower = text.lower() for rule in ROUTING_RULES: for kw in rule["keywords"]: if kw.lower() in text_lower: return rule["engine"] return None这段代码的核心是遍历规则表,返回第一个命中的引擎。为什么返回第一个而不是全部?因为路由层最终只需要选出一个主引擎,后续的置信度自评估会负责校验,如果主引擎不靠谱会触发兜底切换。规则表的顺序会影响优先级,实际维护时要把意图更明确、误命中率更低的规则放在前面。
5.3 Embedding 语义分类路由实现
这一步会进行语义兜底,实现“问法变了也能识别”的能力。需要注意:引擎描述向量要在程序启动时初始化并缓存,避免每个请求都重复计算 Embedding,否则时延和成本都会上升。
# 文件路径:router/semantic_router.py import os import numpy as np from openai import OpenAI client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL") ) EMBEDDING_MODEL = "text-embedding-3-small" _cache = {} def _get_embedding(text: str) -> list: resp = client.embeddings.create( model=EMBEDDING_MODEL, input=text ) return resp.data[0].embedding def _cosine_similarity(vec_a: list, vec_b: list) -> float: arr_a = np.array(vec_a) arr_b = np.array(vec_b) return float(np.dot(arr_a, arr_b) / (np.linalg.norm(arr_a) * np.linalg.norm(arr_b) + 1e-12)) class EngineSelector: def __init__(self, engine_desc_map: dict): self.engine_list = [] self.desc_vectors = [] for engine_name, desc in engine_desc_map.items(): self.engine_list.append(engine_name) if engine_name not in _cache: _cache[engine_name] = _get_embedding(desc) self.desc_vectors.append(_cache[engine_name]) def select(self, user_question: str) -> str: query_vec = _get_embedding(user_question) best_engine = self.engine_list[0] best_score = -1.0 for engine, desc_vec in zip(self.engine_list, self.desc_vectors): score = _cosine_similarity(query_vec, desc_vec) if score > best_score: best_score = score best_engine = engine return best_engine这段代码有一个容易被忽略的点:description 的语料质量直接决定分类准确率。写描述时不要只写“擅长数学”,要写出数学任务的关键词边界,比如“方程、积分、求导、概率统计”。这样向量空间里的数学引擎中心点才会更接近真实用户提问的区域。
5.4 候选答案生成与自评估置信度路由
玩家进入“自评估”阶段,这也是“碰巧蒙对”和“稳定答对”的分水岭。系统先生成候选答案,再由裁判模型打分,低分触发重新选择。
# 文件路径:router/confidence_router.py import json import os from openai import OpenAI client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL") ) CONFIDENCE_THRESHOLD = 0.6 class RouteResult: def __init__(self, answer: str, confidence: float, route_chain: list, status: str): self.answer = answer self.confidence = confidence self.route_chain = route_chain self.status = status def __repr__(self): return json.dumps({ "answer": self.answer, "confidence": self.confidence, "route_chain": self.route_chain, "status": self.status }, ensure_ascii=False, indent=2) def _generate(engine_config: dict, user_question: str) -> str: resp = client.chat.completions.create( model=engine_config["model"], messages=[ {"role": "system", "content": engine_config["system_prompt"]}, {"role": "user", "content": user_question} ], temperature=0.2 ) return resp.choices[0].message.content def _evaluate_confidence(user_question: str, candidate_answer: str) -> float: judge_prompt = f""" 你是一个答案质量控制员。请判断下面回答对用户问题而言是否可靠。 只输出一个 0 到 1 之间的数字,代表整体可靠性,不要输出任何解释。 用户问题:{user_question} 候选回答:{candidate_answer} """ resp = client.chat.completions.create( model=os.getenv("JUDGE_MODEL_NAME", "your-judge-model-name"), messages=[ {"role": "system", "content": "你只输出数字。"}, {"role": "user", "content": judge_prompt} ], temperature=0 ) try: return float(resp.choices[0].message.content.strip()) except ValueError: return 0.0主路由逻辑串联三个阶段:
# 文件路径:main_router.py import json from router.rules_router import match_rule from router.semantic_router import EngineSelector from router.confidence_router import _generate, _evaluate_confidence, RouteResult, CONFIDENCE_THRESHOLD def load_engines_from_file(file_path: str) -> dict: with open(file_path, "r", encoding="utf-8") as f: data = json.load(f) return data["engines"] def route_and_answer(question: str, engine_configs: dict, selector: EngineSelector) -> RouteResult: # 阶段一:规则路由 rule_engine = match_rule(question) route_chain = [] if rule_engine: route_chain.append(f"rule:{rule_engine}") primary_engine = rule_engine else: # 阶段二:语义路由 semantic_engine = selector.select(question) route_chain.append(f"semantic:{semantic_engine}") primary_engine = semantic_engine # 阶段三:生成并自评估,低于阈值则切换引擎兜底 engine_names = list(engine_configs.keys()) for attempt in range(len(engine_names)): current_engine = primary_engine if attempt == 0 else engine_names[attempt] config = engine_configs[current_engine] candidate = _generate(config, question) confidence = _evaluate_confidence(question, candidate) route_chain.append(f"try:{current_engine}") if confidence >= CONFIDENCE_THRESHOLD: return RouteResult( answer=candidate, confidence=confidence, route_chain=route_chain, status="success" ) # 当前引擎不擅长,继续尝试下一个引擎 primary_engine = engine_names[attempt] if attempt == 0 else primary_engine # 所有引擎都不靠谱,返回低置信度结果,交给上层兜底 last_config = engine_configs[engine_names[-1]] last_answer = _generate(last_config, question) return RouteResult( answer=last_answer, confidence=0.0, route_chain=route_chain, status="fallback_needed" ) if __name__ == "__main__": with open("engines.json", "r", encoding="utf-8") as f: configs = json.load(f)["engines"] desc_map = {name: cfg["description"] for name, cfg in configs.items()} selector = EngineSelector(desc_map) test_question = "帮我写一段 Python 代码,读取 CSV 文件并计算每列平均值" result = route_and_answer(test_question, configs, selector) print(result)这段代码完整展示了多级路由的闭环:规则快筛、语义兜底、自评估把关、兜底切换。在真实项目中,你不需要每一层都重新实现,更重要的是理解分层逻辑,然后根据你的业务复杂度裁减。
6. 运行结果与效果验证
运行上述主路由脚本前,请确认已经启动了可用的 OpenAI 兼容 API 服务,并正确配置了环境变量。测试问题时,可以先从规则命中的问题开始,逐步过渡到规则无法命中的问题。
例如,当输入“帮我写一段 Python 代码,读取 CSV 文件并计算每列平均值”时,期望过程是:
- 规则路由命中
code-engine,因为问题中包含Python和代码。 - 直接在
code-engine上生成候选答案。 - 自评估模型给出高于 0.6 的置信度。
- 输出状态为
success,route_chain 大概长这样:
["rule:code-engine", "try:code-engine"]再测试一个规则未命中的问题:“两个分数比较大小,通分后分子怎么变化?”问题中没有“求导”“积分”等关键词,但语义上属于数学。期望过程是:
- 规则未命中。
- Embedding 语义分类选中
math-engine。 - 生成候选答案并通过自评估。
- route_chain 变成:
["semantic:math-engine", "try:math-engine"]这就是一个关键的验证点:如果规则漏判,语义兜底能救回来。如果语义兜底也选错,则自评估阶段的低分数会触发替换引擎,route_chain 中会多出try:记录。
验证失败时先看 route_chain,它能告诉你问题卡在哪一段:
- 如果 chain 里完全没有
semantic,说明规则表误命中了,需要收紧关键词匹配。 - 如果只有
try且后续都是fallback_needed,说明所有引擎都不擅长当前问题,但还有一种可能是裁判模型太严格。 - 如果置信度虚高,比如明显答非所问却给了 0.9,需要更换裁判模型或增加一致性校验逻辑。
7. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 规则路由总是命中错误引擎 | 关键词过于宽泛,上下文限定不足 | 打印规则命中关键词,检查用户输入和 keyword 的重合情况 | 为关键词增加相邻限定词,或降低规则层级优先级 |
| Embedding 分类结果不稳定 | 引擎描述文本太短或太泛 | 打印每个引擎的相似度分数分布 | 扩充描述文本到 50 到 100 字,补充领域典型表达 |
| 置信度远高于预期但不合理 | 裁判模型与生成模型相同,产生自我偏好 | 用不同问题集交叉验证 | 换成更强的、独立的裁判模型,或增加多次采样一致性校验 |
| 每次调用耗时过高 | 每个请求都重复调用 Embedding 接口 | 查看调用日志,确认是否命中缓存 | 启动时将引擎描述向量缓存到内存或向量数据库 |
| 没有命中规则时路由随机性大 | 语义路由缺少相似度阈值下限 | 打印最低相似度数值 | 设置最低阈值,低于阈值直接进入 fallback 而非强行选引擎 |
| 兜底引擎反复重试仍失败 | 引擎列表顺序不合理或兜底策略缺失 | 查看 route_chain 的重试次数 | 限制重试次数,超限后转人工或返回明确“无法回答”话术 |
值得一提的是,很多开发者在初期阶段会出现“规则表膨胀”的问题:一遇到匹配不上就加关键词,加了 100 个关键词后误命中率反而上升。因为自然语言的表达空间很难被有限关键词覆盖,规则表维护成本是线性增长的,但收益是递减的。正确的优化方向是把规则表控制在 10 到 20 条以内,只覆盖业务上最高频、最明确的核心意图,其余交给语义路由。
8. 最佳实践与工程建议
8.1 路由层必须记录完整的链路日志
路由链的价值被严重低估。实际生产中最难排查的问题不是“答错了”,而是“为什么走这条路答错了”。建议每个请求都记录 route_chain、模型名、温度参数、token 用量、耗时和置信度分。这样后续可以做离线归因分析,知道哪一步的决定导致最终结果偏差。
日志记录示例:
{ "request_id": "7f3a2c9e", "question": "两个分数比较大小,通分后分子怎么变化?", "route_chain": ["semantic:math-engine", "try:math-engine"], "confidence": 0.78, "model": "your-math-model-name", "latency_ms": 1520, "need_fallback": false }8.2 不要完全相信模型自评估,要设计多级校验
自评估模型的可靠性并不是百分之百。更稳妥的做法是叠加一致性校验:用同一个模型在 temperature=0.2 和 temperature=0.7 下各生成一次答案,再做语义相似度比对。如果两次结果差异过大,说明模型本身对答案不确定,即使裁判模型给了高分,系统也应当降低最终置信度。这个一致性校验的成本是两次模型调用,适合在高价值请求上开启。
8.3 配置外置,路由规则可热更新
引擎描述、规则表、模型名、阈值都不应该写死在代码里。推荐把这些配置放到配置中心或至少是独立 JSON 文件中。上线后如果发现某个引擎描述匹配效果差,可以直接修改配置文件热加载,而不需要重新发布代码。这会极大缩短线上问题修复周期。
8.4 兜底话术要诚实
当系统进入 fallback_needed 状态时,不要让模型强行编一个答案。按照内容安全底线,面对不确定问题,直接说明“该问题超出当前模型可回答范围”,并引导用户补充信息或转人工,才是生产环境最稳妥的策略。这既避免误导用户,也降低了企业承担错误信息责任的风险。
8.5 安全与权限边界
路由层可能涉及多引擎调用,需要确认每个引擎都有清晰的权限边界。如果某个引擎可以访问企业内部知识库或执行代码,路由层必须增加来源校验和条件限制,不能仅凭用户问题内的关键词放行。所有涉及生产环境的变更,应先在测试环境验证路由收敛情况,再进行灰度发布。
9. 总结与后续学习方向
“它只是碰巧蒙对”这句话,放在工程语境里是一个好的提醒。它提醒我们,大模型系统的可靠性不是靠单一模型的能力堆积,而是靠“能力边界识别 + 任务路由 + 置信度校验 + 兜底策略”的组合设计。本文给出的三层路由方案,在成本可控的前提下,把“稳定答对”的工程概率提上去,并在模型不擅长时主动承认局限。这就是“下次我要选我擅长的”的工程形态。
后续可以继续深入几个方向:把 Embedding 语义路由升级为基于微调分类模型的意图识别;在自评估阶段引入更复杂的 Multi-Agent 讨论机制;或者在路由前增加缓存层,把常见高频问题在到达模型前直接命中答案库,进一步降低成本和时延。建议你先从本文的最小示例跑起,把 route_chain 日志打全,然后基于日志持续迭代规则表和引擎描述。工程能力不是靠一个聪明的模型瞬间获得的,而是靠一层一层守住错误累积出来的。