TypeScript · Open Council

所有中文关键词都是死代码——路由、角色与一个正则坑

Open Council 复盘:怎么判断一个问题该辩几轮、该派谁上、给模型分什么角色。从一个「\b 让全部中文路由词失效」的经典正则坑讲起。

Open Council 收到一个问题,要先决定三件事:辩几轮(quick 还是 debate)、派几个模型上、每个模型演什么角色。这套路由全靠关键词启发式,零 LLM 成本。可它藏着一个经典的正则坑——所有中文路由关键词,曾经是彻底的死代码。

\b 认不出中文

路由的第一步是给问题分类:code / architecture / security / comparison / math / general。判据是关键词表,比如架构类命中「架构、设计、微服务、可扩展」,安全类命中「注入、鉴权、漏洞」。

最初的匹配器把每个关键词用 \b...\b(词边界)包起来——这是英文里的标准写法,防止 code 命中 encoder 里的子串。问题是:\b 只认 ASCII 单词字符。一个汉字和它的邻居之间,永远构不成 \b 边界。 于是任何被 \b 包住的中文关键词,永远匹配不到。整张中文路由表写了等于没写,直到有人拿中文问题去测才暴露。

修法是把关键词按语言拆两组,用不同规则拼成一个正则:

\b(?:en_keywords)\b | (?:zh_keywords)

英文组保留 \b(否则 code 又会误伤 encoder),中文组裸匹配(字面量,不加边界)。这就是关键词表必须 en / zh 分列的根本原因——不是为了好看,是因为两种语言的「词边界」根本不是一回事。

中文一个字,顶英文两个半

同一个坑还有第二层。判断问题复杂度要看长度:短问题走 compare,长问题(架构类超过一定长度)才升到 debate。可长度阈值当初是按英文调的,直接套中文会严重低估——中文信息密度高,同样字数承载的内容多得多。

所以长度不用原始字符数,用一个加权的 effectiveLength每个 CJK 字符按 2.5 计,其余按 1 计。 配套把架构/安全类的长度门槛压到很低——一个 17 个汉字的架构问题(加权后约 43),必须能够到 debate 那条线。关键词已经确认了问题有实质内容,长度门只是用来滤掉「架构好吗?」这种一句话,所以门压得很低。

派谁上:prefer 排序 + 席位约束

问题分好类,接着排候选模型。排序由配置里的 prefer(一个有序偏好列表)驱动:

  • 两个模型都在 prefer 里 → 按列出的先后排;
  • 只有一个在 → 它靠前;
  • 都不在 → 先按能力档降序,再按 priority 升序(数字越小优先级越高)。

prefer 有个容易漂移的坑:模型还在、但 prefer 列表没跟着更新(比如重新扫描发现了新模型,却没加进 prefer)。对策是所有构造 prefer 的入口统一去重,且重扫时把新模型补进去,而不是漏掉。

席位数按模式和配置求交集:quick 固定 1 席,compare 至少 2 席,debate 至少 3 席,上限取「模型数」和「配置 max」的较小值。模型不够导致 min > max 时,把 min 夹到 max,并记一条降级事件——现实里模型数量不够是常态,与其报错,不如夹紧并如实告诉用户「降级了」。

分角色:让一个便宜模型来设计这场辩论

角色(分析师、工程师、创新者……)不是写死的,默认走一条「LLM 设计面板」的路:先让一个模型读所有参与模型的画像,再由它设计出这场辩论该有哪些角色、每个角色配哪个模型。

一个反直觉的取舍:设计面板的这个模型,故意不用最强的那个,优先选中等档(balanced)的。 理由是设计面板不需要顶配智力——够聪明能推理出「推理重的角色配强模型、要速度的角色配快模型」就行,而中等档更便宜更快。把顶配模型省下来干正事。

角色的多样性靠 prompt 里的硬约束保证:「每个角色必须有独特且对立的视角,他们应该在关键点上分歧」「制造有建设性的张力,而不是冗余的附和」「尽量把角色分散到不同的模型/供应商」。席位数也交给 LLM 在区间内定,并明确要求「选能产生有效分歧的最小数量,别用冗余角色凑满上限」。LLM 失败或没有可用模型时,回退一套硬编码的中文角色(🔍 分析师、⚙️ 工程师、💡 创新者、🎯 批评家、📐 务实派)轮询分配。

又一个边界坑:gpt-5 不该吞掉 gpt-5-nano

LLM 设计完角色,会在 JSON 里回填「这个角色派给哪个模型」。它给的名字可能不精确,于是要把它解析回一个真实模型。

早期实现用朴素的双向 includes,结果 gpt-5 会静默落到 gpt-5-nano 上,反过来 gpt-5 也会吞掉 gpt-50——一次角色分配就悄悄派错了模型。修法是一个边界安全的前缀匹配:前缀后面必须跟 -.,或者字母到数字的跃迁(gptgpt4),且数字连跑算同一个数(gpt-5 不会吞 gpt-50);多个候选时取 id 最短的(最不特化的那个家族成员)。这套规则被抽到一个共享模块,让路由和 API 调用两处共用同一套边界判定,避免两边各写一份、慢慢漂移。

顺带一个诚实的小注:resolveMode 的注释里写阈值是 50 / 120 / 30,但实际架构/安全门用的是 40——注释没跟上代码改动。路由这类「一堆魔法数字」的地方,注释漂移几乎是必然,值得定期对一遍。

小结

从一个问题到「谁演什么角色」,路由这条链路上的几个决策与坑:

  • \b 认不出中文:词边界只认 ASCII,中文关键词得裸匹配,en / zh 必须分组;
  • 中文按 2.5 倍加权:长度阈值按英文调,套中文要用 effectiveLength 加权,否则系统性低估复杂度;
  • prefer 会漂移:模型在但偏好列表没同步,统一去重 + 重扫补齐;
  • 让便宜模型设计辩论:设计面板用中等档而非顶配,够聪明又省钱,顶配留着干正事;
  • 模型名匹配要边界安全gpt-5 不能吞 gpt-5-nano,规则抽出来两处共用防漂移。

一句话:跨语言、跨模型的字符串匹配,坑几乎都在「边界」上——英文的词边界、中文的信息密度、模型名的版本边界,每一个想当然都会变成一个静默的错。

留言

  • 加载中…

留言先审后发,通过后公开显示;邮箱只有站主可见。