[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fCoyd1kYEmdC3akVGD6gC35-mt63NCsDbOq5f5zcisiw":3,"$fOMyxalSDISbvzFIFuA6nXHLHuVyertAkipMws0H0YOU":44},[4],{"id":5,"type":6,"title":7,"slug":8,"summary":9,"body":10,"coverUrl":11,"productScreenshots":12,"productLinks":13,"authorName":14,"authorUrl":15,"authorSubject":16,"category":17,"tags":22,"sourceLabel":35,"sourceName":36,"sourceUrl":36,"status":37,"seoTitle":36,"seoDescription":36,"canonicalUrl":36,"isFeatured":38,"sno":39,"sortOrder":40,"publishedAt":41,"updatedAt":42,"createdAt":43},"525e9d4d-50ba-48c4-be55-4810590d714b","article","AI Agent 可观测性：如何知道它到底在哪一步出错","genai-agent-observability-with-opentelemetry","Agent 的一次回答可能经过多次模型调用、检索、工具执行和重试。本文从日志、指标与 Trace 的分工讲起，介绍 OpenTelemetry 的 GenAI 语义约定、失败排查方法、敏感内容采集边界，以及如何把 AI 运行变成可解释的执行链路。","## 当 Agent 答错时，先别急着换模型\n\n一个 Agent 花了 45 秒才回答一个简单问题，原因可能完全不同：模型本身慢、检索服务慢、工具重试了三次、上下文被塞得太长，或者多个步骤串行执行导致整体延迟被放大。\n\n如果系统只有一条“请求失败”日志，你无法知道问题发生在哪里。AI 应用的可观测性，不能只记录最终答案，而要记录一次 Agent 运行中发生过的模型调用、工具调用、检索、重试和输出。\n\n![OpenTelemetry 标志](https:\u002F\u002Fopentelemetry.io\u002Fimg\u002Flogos\u002Fopentelemetry-horizontal-color.png)\n\nOpenTelemetry（简称 OTel）正在为 GenAI 场景补充语义约定（Semantic Conventions），把“模型名称”“输入输出 Token”“工具调用”和“Agent 工作流”等信息用统一字段记录。OpenTelemetry 的[官方实践文章](https:\u002F\u002Fopentelemetry.io\u002Fblog\u002F2026\u002Fgenai-observability\u002F)展示了如何把一次 LLM 调用放进普通服务的 Trace 中。\n\n## 日志、指标和 Trace 各自回答什么\n\n三种信号不是互相替代的：\n\n- **日志**回答“某一刻发生了什么”，适合记录错误详情和业务事件。\n- **指标**回答“整体趋势怎样”，适合看延迟、Token 消耗、错误率和调用量。\n- **Trace**回答“一次请求经过了哪些步骤”，适合定位 Agent 的链路瓶颈。\n\n对传统 Web 请求来说，一条 Trace 可能是“网关 → API → 数据库”。对 Agent 来说，它更像：\n\n```text\n用户请求\n  └─ Agent 工作流\n      ├─ LLM：判断是否需要检索\n      ├─ Retriever：查询知识库\n      ├─ LLM：生成工具参数\n      ├─ Tool：调用订单 API\n      ├─ LLM：整理结果\n      └─ 最终回答\n```\n\n没有这条树状链路，工程师只能靠猜。\n\n## GenAI 语义约定记录了什么\n\n具体字段仍处于演进中，但常见信息包括：\n\n- 请求使用的模型与服务商。\n- 输入 Token、输出 Token 和调用持续时间。\n- 模型停止原因，例如正常结束或发起工具调用。\n- Agent、Workflow、Session 和工具的标识。\n- 检索、工具执行和模型调用之间的父子关系。\n\n例如，一次慢请求可以被拆成：模型调用 1.2 秒，向量检索 0.4 秒，订单 API 8 秒，模型总结 1.1 秒。你不必猜“是不是模型变慢了”，因为 Trace 会直接显示大部分时间花在订单 API 上。\n\n```mermaid\nflowchart TD\n    Q[\"用户请求\"] --> T[\"Agent Trace\"]\n    T --> L1[\"LLM：理解意图\"]\n    L1 --> R[\"检索或工具调用\"]\n    R --> L2[\"LLM：生成下一步\"]\n    L2 --> C{\"成功完成?\"}\n    C -->|否| E[\"记录错误与重试原因\"]\n    C -->|是| M[\"记录结果、Token 与耗时\"]\n    E --> F[\"返回或进入受限重试\"]\n    M --> F\n```\n\n## 一次失败应该怎样排查\n\n假设用户投诉：“客服 Agent 这次答非所问。”可以按下面顺序看 Trace：\n\n### 先看输入是否正确\n\n检查系统指令、用户问题、历史摘要和检索片段是否真的进入了模型上下文。很多所谓“模型幻觉”，根源是检索为空、字段被截断，或者把旧版本政策混进了当前请求。\n\n### 再看工具是否返回正确\n\n工具调用成功不代表业务结果正确。HTTP 状态码是 200，不等于订单查询返回了正确用户的数据。Trace 里应记录工具名称、版本、参数摘要、耗时和错误类型；对于敏感值，只记录哈希、字段名或脱敏后的摘要。\n\n### 最后看模型是否正确使用上下文\n\n如果上下文里有正确资料，模型仍然选错工具或忽略约束，才更像提示设计、模型能力或路由策略的问题。此时可以把同一个 Trace 送进离线评测，比较不同模型、提示模板和工具描述。\n\n## 一条 Agent Trace 应该长什么样\n\n不要把所有信息都塞到一个巨大的 Span 里。更容易排查的结构，是为一次用户任务建立根 Span，再按执行层级嵌套：\n\n```text\nagent.run\n├── retrieval.query\n├── gen_ai.chat\n│   └── tool.call\n├── tool.execute\n└── gen_ai.chat\n```\n\n每个 Span 记录“这个步骤做了什么”和“花了多少时间”，而不是默认保存全部内容。模型 Span 可以记录模型名、响应状态、输入输出 Token 和结束原因；检索 Span 可以记录索引名、Top-K、过滤条件摘要和命中文档 ID；工具 Span 可以记录工具版本、参数校验结果、外部响应码和重试次数。\n\n把字段分成低基数和高基数也很重要。模型名、操作类型和错误类别适合做指标标签；完整用户问题、订单号和工具参数不适合直接作为指标标签，否则时间序列数量会爆炸。高基数信息应该放在受控的日志或事件里，并设置访问权限。\n\n## 从代码到 Trace：先包住边界，再追求完整\n\n第一版 instrumentation 不需要覆盖整个 Agent 框架。可以先包住三个边界：\n\n```python\nwith tracer.start_as_current_span(\"agent.run\") as run:\n    result = call_model(messages)\n    record_model_usage(run, result.usage)\n\n    with tracer.start_as_current_span(\"tool.execute\") as tool_span:\n        tool_span.set_attribute(\"tool.name\", tool_name)\n        tool_result = execute_tool(args)\n\n    final = call_model(messages + [tool_result])\n```\n\n关键不是这段代码本身，而是让每次模型调用和工具调用都继承同一个 Trace 上下文。否则你会得到一堆互相孤立的请求记录，仍然无法回答“这次回答经过了哪个工具”。\n\n接下来再补齐重试、检索和人工接管。每加一类信号，都应该配一个排查问题：它能不能帮助定位慢、错、贵或不安全？如果不能，就不要为了“字段齐全”增加采集复杂度。\n\n## 内容采集是双刃剑\n\n记录完整 Prompt 和模型输出，对调试非常有帮助；但这些内容也可能包含个人信息、商业机密、访问令牌和用户输入的恶意指令。\n\nOpenTelemetry 的 GenAI 指南特别强调，默认可以只记录模型名、Token 和耗时，只有明确开启内容采集时才保存完整消息、工具参数和工具结果。生产环境建议分层：\n\n- 默认记录元数据，不记录原文。\n- 调试租户或抽样请求才采集内容。\n- 对邮箱、手机号、订单号和密钥做脱敏。\n- 限制 Trace 的保存时间和访问角色。\n- 严禁把完整 Prompt 直接打进普通应用日志。\n\n可观测性本身也必须经过威胁建模，否则为了排查 AI 问题，反而建立了一个更大的数据泄露面。\n\n一种实用的内容策略是“默认摘要、按需取原文”：Trace 默认只保存消息长度、哈希、敏感字段数量和版本号；当用户授权调试时，再从加密的短期存储中关联原文。这样既能判断“上下文是否变长、是否发生了重试”，也不会让每个监控面板都暴露完整对话。\n\n如果确实需要记录工具结果，应优先保存经过裁剪的结构化摘要。例如只保存 HTTP 状态、返回字段集合和结果条数，不保存完整客户资料。对于安全事件，还可以保存触发规则和脱敏后的攻击片段，让安全团队能复盘，不让普通业务角色看到原始隐私数据。\n\n## 指标应该如何设计\n\n至少需要四类指标：\n\n1. **延迟**：首 Token 延迟、完整响应延迟、工具调用延迟。\n2. **消耗**：输入输出 Token、缓存命中、模型调用次数。\n3. **可靠性**：超时、解析失败、工具失败、重试次数和人工接管率。\n4. **质量代理指标**：引用覆盖率、结构化输出校验率、拒答率和离线评测分数。\n\n不要把“Token 越少越好”当成唯一目标。压缩上下文可能降低成本，却也可能删掉回答所需的证据。正确的做法是同时看质量、延迟和成本，按用户任务分组，而不是只看全局平均数。\n\n这些指标还需要和“请求类型”绑定。客服问答、代码生成、文档摘要和事务执行的正常范围不同，混在一起看会把异常平均掉。建议至少按 Agent、任务类型、模型版本和租户分组，并同时看 P50 与 P95。平均延迟正常，不代表最慢的那 5% 用户没有一直卡住。\n\n成本归因也不要只用一个总金额。可以把一次任务的成本拆成模型成本、检索成本、工具调用成本和重试成本。这样当账单上升时，你才能判断应该缩短上下文、调整模型路由、修复工具超时，还是限制某一类 Agent 的最大步数。\n\n## 与传统 OTel 的关系\n\nGenAI 语义约定不是一套新的监控后端，也不是要求你换掉现有的 Jaeger、Prometheus 或 OTLP Collector。它更像是一组让不同厂商“说同一种字段语言”的约定。\n\n因此，落地可以从现有链路开始：给每次 Agent 运行创建一个根 Span，把模型调用和工具调用作为子 Span，再把 Token、模型版本和错误原因写入标准属性。这样未来更换可观测性后端时，数据仍然能迁移。\n\n不过要注意，语义约定仍在快速发展，字段的稳定级别可能变化。建议把属性名集中封装在自己的 instrumentation 层，不要在几十个业务文件里散落字符串。\n\n## 从观测到自动修复\n\n可观测性最终不只是给人看，还可以成为控制回路：\n\n- 工具连续超时，自动降低该工具的并发并切换备用路径。\n- 输入 Token 接近预算，先压缩历史，再决定是否升级模型。\n- 结构化输出连续校验失败，暂停自动执行，转人工处理。\n- 某个模型版本的错误率显著上升，按流量比例回滚到上一版本。\n\n但自动修复必须有边界。不要让 Agent 根据自己记录的 Trace 无限调整权限、提示词或工具列表。观测数据应该进入经过审核的策略层，由明确的阈值、审批和回滚机制控制变更。\n\n## 结语：从“答案错了”走向“哪一步错了”\n\nAI Agent 的可观测性不是给日志加几个 Token 字段，而是把一次非确定性运行还原成可解释的执行链路。工程团队真正需要的不是知道“模型很慢”，而是知道“哪一个模型调用、哪一次检索、哪一个工具、哪一次重试造成了这次慢”。\n\n建议先选一个高价值流程，建立最小 Trace：请求 ID、Agent ID、模型名、输入输出 Token、工具名、耗时和错误类型。等链路稳定后，再逐步加入内容采集、评测结果和成本归因。\n\n**落地清单：**\n\n- 是否能看到一次 Agent 运行的完整步骤树？\n- 是否能区分模型慢、工具慢和重试造成的慢？\n- 是否记录了模型版本、提示版本和工具版本？\n- 是否默认关闭敏感内容采集？\n- 是否把 Trace 与离线评测样本关联起来？\n\n## 一手资料\n\n- [OpenTelemetry GenAI 可观测性实践](https:\u002F\u002Fopentelemetry.io\u002Fblog\u002F2026\u002Fgenai-observability\u002F)\n- [OpenTelemetry GenAI 属性与语义约定](https:\u002F\u002Fopentelemetry.io\u002Fdocs\u002Fspecs\u002Fsemconv\u002Fregistry\u002Fattributes\u002Fgen-ai\u002F)\n- [OpenTelemetry 语义约定总览](https:\u002F\u002Fopentelemetry.io\u002Fdocs\u002Fspecs\u002Fsemconv\u002F)","\u002Fuploads\u002F2026-08-05\u002F19586c04-7c5c-483a-8cdf-0860e7a03918.jpg",[],[],"Foundit AI","https:\u002F\u002Ffoundit.cn","f39339b1-aaa6-4e86-b0c2-a6e6a21113b5",{"id":18,"name":19,"slug":20,"description":21},"6179d3b6-dc34-4483-9ded-3cd9f1b37a47","科普","abbreviation","介绍各领域新兴概念",[23,27,31],{"id":24,"name":25,"slug":26},"0848beb4-db26-4fb8-b391-f852a11be192","AI编程","ai-coding",{"id":28,"name":29,"slug":30},"7c76bfc2-f80f-4ee0-a95d-27bd8708b434","技术","slug",{"id":32,"name":33,"slug":34},"4c2bbea6-eab7-40a8-8447-1de478ff7749","分析","analyse","资料来源",null,"published",false,64,0,"2026-07-31T00:00:00.000Z","2026-08-05T04:11:43.185Z","2026-08-05T02:13:45.344Z",[45,63,85],{"id":46,"type":6,"title":47,"slug":48,"summary":49,"body":50,"coverUrl":51,"productScreenshots":52,"productLinks":53,"authorName":14,"authorUrl":15,"authorSubject":16,"category":54,"tags":55,"sourceLabel":36,"sourceName":36,"sourceUrl":36,"status":37,"seoTitle":36,"seoDescription":36,"canonicalUrl":36,"isFeatured":38,"sno":59,"sortOrder":40,"publishedAt":60,"updatedAt":61,"createdAt":62},"a3c11b62-9668-40cf-822d-25787f994c75","A2A：当 Agent 开始互相「递名片」","a2a-agent-to-agent-protocol","MCP 让 AI 统一接上工具，却没解决 Agent 之间怎么分工。A2A（Agent-to-Agent 协议）用「Agent Card 名片」让智能体互相发现、委派任务、协作交付。","你有没有想过：当公司里不止一个 AI Agent，而是几十个，它们该怎么分工？谁负责查天气、谁负责排日程、谁负责写代码？如果让它们各自为战，那不过是把「一个人的孤岛」换成「一群人的孤岛」。\n\n一个叫 A2A 的协议正在解决这个问题——它让 Agent 之间能像人一样「互相介绍、认领任务、协作交付」。\n\n## MCP 解决了「接工具」，没解决「连同伴」\n\n我们先前聊过 MCP（模型上下文协议）：它像 USB-C，让 AI 能统一地连上各种工具——读代码、查数据库、调 API。但 MCP  deliberately 不回答另一个问题：Agent 和 Agent 之间怎么发现彼此、怎么分工？\n\nhttps:\u002F\u002Ffoundit.cn\u002Farticle\u002Fmcp-ai-usb-c-moment\n\n举个例子。你问一个「个人助理 Agent」：「帮我看看明天北京的天气，如果下雨就改到室内，并把会议邀约发给团队。」这个助理自己未必会看天气、也不该直接改所有人的日历。更合理的做法是：它去找到「天气 Agent」和「日历 Agent」，把子任务委派给它们，再把结果拼起来回答你。A2A（Agent-to-Agent Protocol，智能体到智能体协议）就是干这件事的标准。\n\n## A2A 是什么\n\nA2A 由 Google 在 2025 年 4 月提出，几个月后捐给 Linux 基金会，和 MCP 一样进入了中立治理。它的核心思想非常像现实中的名片交换：\n\n- 每个 Agent 都发布一张机器可读的 **Agent Card（名片）**，声明自己叫什么、能做什么、接受什么格式的输入、返回什么、需要怎样的鉴权。\n- 一个「编排 Agent」读到这些名片，就知道「这个任务该交给谁」。\n- 然后它通过 JSON + HTTP 协议把任务委派过去，支持长任务、流式结果和多轮对话。\n\n业界给的类比很精准：**MCP 连接 Agent 与工具，A2A 连接 Agent 与同伴。** 工具是被「调用」然后返回；同伴是被「委派」然后协商。\n\n2026 年 4 月，A2A 发布了 **1.0 版本**，成为稳定的生产标准，并带来了「带签名的 Agent Card」用于可验证身份。一年之内已有 150+ 组织在生成环境运行它，IBM 自家的 Agent Communication Protocol 也在 2025 年 8 月合并进了 A2A，没有让这一层 fragmentation（碎片化）。\n\n## 它是怎么运作的：发现 → 委派 → 交付\n\n整个协作流程可以拆成三步，用一张图就能看明白：\n\n```mermaid\nflowchart LR\n    U[用户需求] --> O[编排 Agent]\n    O -->|读取 Agent Card| C[天气 Agent]\n    O -->|读取 Agent Card| I[日历 Agent]\n    O -->|委派子任务| C\n    O -->|委派子任务| I\n    C --> R1[天气结果]\n    I --> R2[日程结果]\n    R1 --> O\n    R2 --> O\n    O --> A[汇总后回答用户]\n```\n\n落到代码层面，Agent Card 就是一份 JSON。比如一个天气 Agent 的名片可能长这样：\n\n```json\n{\n  \"name\": \"天气 Agent\",\n  \"description\": \"提供全球城市天气查询\",\n  \"url\": \"https:\u002F\u002Fweather-agent.example\u002Fa2a\",\n  \"capabilities\": { \"streaming\": true },\n  \"skills\": [\n    {\n      \"id\": \"get_weather\",\n      \"name\": \"查询天气\",\n      \"examples\": [\"北京今天天气如何？\"]\n    }\n  ],\n  \"authentication\": { \"schemes\": [\"Bearer\"] }\n}\n```\n\n编排 Agent 拉取这张名片后，就知道「查天气」这个技能由谁提供、去哪个地址调用、要带什么鉴权。它把用户问题拆成子任务，分别委派，再把各 Agent 的回包汇总成最终答案。长任务还能流式返回进度，不必干等。\n\n## 它能做什么，做不了什么\n\nA2A 解决的是「信封」问题——怎么发现同伴、怎么把任务送过去、怎么收回结果。但它有意**不定义「信封里写什么」**：两个 Agent 之间到底该用怎样的语义去沟通、任务怎么拆解，是高于协议层的事。\n\n- **强项**：跨厂商、跨框架。无论 Agent 是用 LangGraph、CrewAI、LlamaIndex 还是微软、谷歌的框架写的，只要都讲 A2A，就能互相委派。主流 agent 框架已原生支持。\n- **边界**：协议标准化的是「通信格式」，不是「协作智能」。任务拆得好不好、委派得对不对，仍然取决于编排 Agent 本身的设计。\n- **补充视角**：也有人提出基于 W3C 去中心化身份（DID）的替代方案，觉得 A2A 的模型「太像传统 Web」。但在企业多 Agent 系统里，A2A 已经是事实上的默认答案。\n\n## Tips\n\n- 记住分层：想接工具看 **MCP**，想让 Agent 互相协作看 **A2A**——两者互补，不是替代。\n- 设计多 Agent 系统时，先画清「谁发布名片、谁做编排、任务怎么拆」三件事，再选框架。\n- 评估一个 Agent 平台是否「能协作」，看它是否支持 A2A 1.0 与签名 Agent Card（身份可验证很重要）。\n- 别指望协议替你做任务规划：A2A 管「送信」，拆任务的逻辑要你自己写或交给编排模型。\n- 落地节奏上，先把 MCP 接好让单个 Agent 能干活，再用 A2A 把多个能干的 Agent 织成网络——这是 2026 年最主流的演进路径。","https:\u002F\u002Foxqtewbrpuiouqqjrvdv.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fpublic-media\u002F2026-07-19\u002F7819028f-dd6f-4988-9964-56134773dc53.jpg",[],[],{"id":18,"name":19,"slug":20,"description":21},[56,57,58],{"id":24,"name":25,"slug":26},{"id":32,"name":33,"slug":34},{"id":28,"name":29,"slug":30},75,"2026-07-17T00:00:00.000Z","2026-07-20T01:14:37.908Z","2026-07-19T16:17:09.511Z",{"id":64,"type":6,"title":65,"slug":66,"summary":67,"body":68,"coverUrl":69,"productScreenshots":70,"productLinks":71,"authorName":14,"authorUrl":15,"authorSubject":16,"category":72,"tags":73,"sourceLabel":35,"sourceName":36,"sourceUrl":36,"status":37,"seoTitle":36,"seoDescription":36,"canonicalUrl":36,"isFeatured":38,"sno":81,"sortOrder":40,"publishedAt":82,"updatedAt":83,"createdAt":84},"1d21b863-e352-417e-81f6-3a8abcb73dd6","Bridge API","bridge-api","本地运行的 AI Provider Runtime —— 一个 API，连接所有 AI 网页","## 1. 概述与定位\n\n**Bridge API** 是一个运行在用户本机的 **AI Provider Runtime**。它对外提供统一的 **OpenAI Compatible API**（`\u002Fv1\u002Fchat\u002Fcompletions`、`\u002Fv1\u002Fmodels`），并在内部通过 Chrome 扩展把请求转发给已经登录的 **DeepSeek 网页**。账号、Cookie 与项目文件全程不离开本机，Bridge 不保存账号、不代理账号、不托管 Cookie。\n\n| 它“不是”什么 | 它“是”什么 |\n| --- | --- |\n| 不是 DeepSeek 官方 API（无需 API Key） | AI Provider Runtime，DeepSeek 只是第一个 Provider |\n| 不是单纯浏览器插件（插件只控制网页） | 本地文件系统 \u002F Git 能力的受保护网关 |\n| 不是一个被绑定的模型（Provider 无关） | 让任意 OpenAI 客户端直接调用浏览器里的 AI |\n\n> **核心设计原则**：Provider 无关、本地优先、开放接口、模块化扩展、最小权限。所有项目读取、Git 分析、上下文构建均在本地完成。\n\n## 2. 系统架构\n\nBridge API 由三层组成：左侧任意 OpenAI 客户端、中间本地 Runtime、右侧浏览器与本地项目。Runtime 是唯一的“大脑”，所有业务逻辑都收敛在这里；浏览器扩展只是 DOM 控制终端，Native Host 是最小权限的文件系统代理。\n\n```mermaid\nflowchart TD\n  C[OpenAI 兼容客户端\u003Cbr\u002F>Cursor \u002F VS Code \u002F CLI\u003Cbr\u002F>Cherry Studio \u002F Open WebUI\u003Cbr\u002F>LangChain]\n  subgraph R[Bridge API Runtime]\n    direction TB\n    GW[API Gateway · Node http]\n    PM[Provider Manager · Agent Engine]\n    BB[Browser Bridge · Context Engine]\n    TE[Tool Engine · Project Engine]\n    GE[Git Engine · Security Engine]\n    Q[Single Task Queue · 配置]\n  end\n  E[Chrome 扩展 MV3\u003Cbr\u002F>background.js + content.js]\n  D[DeepSeek 网页\u003Cbr\u002F>chat.deepseek.com]\n  N[Native Host\u003Cbr\u002F>文件\u002FGit 白名单]\n  P[本地授权项目\u003Cbr\u002F>路径\u002F敏感保护]\n  C -->|HTTP \u002F SSE| R\n  R -->|指令 \u002F 事件| E\n  E -->|DOM 控制| D\n  R -. 规划 .-> N\n  N -->|文件 \u002F Git| P\n  R -. 受保护访问 .-> P\n```\n\n**关键解耦点**：扩展不持有文件系统权限，文件访问要么由 Runtime 直接执行（当前 MVP），要么经由 Native Host 的白名单协议；浏览器侧只负责把 Prompt 写进网页、把回答读出来，业务规则（鉴权、队列、路径安全、工具解析）全部在 Runtime 完成。\n\n## 3. 技术栈与工程结构\n\n项目是一个 npm **workspaces** 单仓（monorepo），包含 `apps\u002F*`、`packages\u002F*`、`providers\u002F*` 三组包。运行时直接以 `node --experimental-strip-types` 执行 TypeScript（Node 22+），无需构建步骤；类型检查用 `tsc --noEmit`。\n\n```\nbridge-api\u002F\n├── apps\u002F\n│   ├── runtime\u002F        # 核心 Runtime 入口（src\u002Findex.ts, server.ts, config.ts）\n│   │   └── public\u002F     # 本地图形控制台（index.html \u002F app.js \u002F styles.css）\n│   ├── cli\u002F            # bridge CLI（纯 HTTP 客户端）\n│   ├── extension\u002F      # Chrome MV3 扩展（background.js, content.js, popup.*）\n│   └── native-host\u002F    # 最小权限 Native Messaging Host（协议骨架）\n├── packages\u002F\n│   ├── protocol\u002F       # 类型契约：ChatMessage, ChatDelta, Provider, BrowserCommand\u002FEvent\n│   ├── provider-manager\u002F  # Provider 注册、切换、健康检查\n│   ├── browser-bridge\u002F    # Runtime ↔ 扩展的命令\u002F事件桥\n│   ├── agent-engine\u002F      # 多轮代码代理 + 工具协议解析\n│   ├── context-engine\u002F    # ripgrep 检索 + 上下文构建\n│   ├── project-engine\u002F    # 多项目管理、路径保护、读写替身脱敏\n│   ├── tool-engine\u002F       # 工具执行、待批准变更\n│   ├── git-engine\u002F        # git status \u002F diff 封装\n│   ├── security-engine\u002F   # 敏感路径判定、密钥脱敏\n│   ├── queue\u002F             # 单任务串行队列\n│   └── shared\u002F            # createId \u002F toErrorMessage \u002F expandHome\n├── providers\u002F\n│   └── deepseek\u002F       # DeepSeekWebProvider（实现 Provider 契约）\n├── tests\u002F              # core.test.ts（node --test）\n├── PRD.md  README.md  progress.md  package.json  tsconfig.json\n```\n\n各 `packages\u002F*` 之间是显式的依赖关系（通过相对 `..\u002F..\u002Fpackages\u002Fxxx\u002Fsrc\u002Findex.ts` 导入），运行时由 `apps\u002Fruntime\u002Fsrc\u002Findex.ts` 统一实例化并注入：\n\n```ts\nconst browser   = new BrowserBridge();\nconst projects  = new ProjectEngine(config.workspace, config.projects);\nconst git       = new GitEngine();\nconst providers = new ProviderManager([new DeepSeekWebProvider(browser)], config.provider);\nconst context   = new ContextEngine(projects, git);\nconst tools     = new ToolEngine(projects, context, git);\nconst agent     = new AgentEngine(providers, projects, tools);\nconst server    = createRuntimeServer({ config, browser, providers, projects, context, git, queue, tools, agent });\n```\n\n## 4. 一次对话请求的完整生命周期\n\n所有请求进入 `createRuntimeServer` 的单一 Node `http` 处理器。下面以 `POST \u002Fv1\u002Fchat\u002Fcompletions` 为主线，展示从鉴权到 SSE 输出的全过程。\n\n```mermaid\nflowchart TD\n  A[POST \u002Fv1\u002Fchat\u002Fcompletions] --> B[鉴权: Bearer \u002F x-bridge-token]\n  B -->|否| B1[401 未授权]\n  B -->|是| C[校验 model == active.model]\n  C --> D{是代码请求?}\n  D -->|否| E[普通对话: providers.active.stream]\n  E --> E1[SSE 直出]\n  D -->|是| F[进入 SingleTaskQueue 排队]\n  F --> G[AgentEngine.stream 多轮循环]\n  G --> H{生成待批准变更?}\n  H -->|是| H1[等待人工审批]\n  H -->|否| H2[SSE 流式回答 \u002F 工具结果]\n```\n\n> **SSE 通道约定**：模型正文走 `data:` 帧；Bridge 内部状态（队列位置、心跳）走 **注释帧** `: bridge-status ...`，避免被 OpenAI 客户端误并入回答。OpenAI 客户端会把每个 `data:` 帧当作模型输出，因此内部状态必须放在注释帧里，只有图形控制台会读取并展示。\n\n## 5. 多轮本地代码代理\n\n当请求被判定为“代码请求”（`bridge.mode=code` 或末条消息命中 `code|repo|项目|文件|修改|…` 等关键词）时，`AgentEngine` 接管，进入一个最多 16 轮的工具循环。核心目标是让网页版 DeepSeek 既能“读”本地项目，又不会未经授权地改文件。\n\n```mermaid\nflowchart TD\n  A[第 N 轮开始] --> B[仅发送最新轮次给 DeepSeek 网页]\n  B --> C[收集回答 含120s单轮超时]\n  C --> D[解析 BRIDGE_TOOL 信封 多格式]\n  D --> E{检测到工具调用 且通过协议校验?}\n  E -->|否| E1[返回最终回答]\n  E -->|是| F{客户端提供工具?}\n  F -->|是| F1[转为 OpenAI tool_calls 交客户端执行]\n  F -->|否| G[本地 ToolEngine 执行]\n  G --> H{是写操作?}\n  H -->|是| H1[生成待批准变更 \u002F 直写]\n  H -->|否| H2[结果作为 tool 消息回灌]\n  H2 -->|循环 不超过16轮| B\n```\n\n实现要点：\n\n- **状态页面复用**：DeepSeek 网页本身是有状态的会话，所以每一轮只把“最新工具结果 \u002F 修复指令”发给网页，绝不回放完整对话历史（否则它会重复回答早期问题）。见 `providers\u002Fdeepseek\u002Fsrc\u002Findex.ts` 的 `promptForWebConversation`。\n- **协议自愈**：若 DeepSeek 返回的工具指令不符合规范，Agent 会把它作为 `assistant` 消息连同修复提示再发一次，最多修复 2 次；仍失败则拒绝执行、不做任何文件修改。\n- **去重**：模型可能在同一回答里以“规范信封 + 渲染兼容形式”重复同一动作，`uniqueToolCalls` 只执行一次，但会保留刻意不同的多次 Edit。\n- **写保护**：Write\u002FEdit 在默认配置下只生成“待批准变更”；仅当项目开启 `autoApplyWrites` 才直接落盘。\n\n## 6. BRIDGE TOOL PROTOCOL V1\n\n网页版模型无法直接调用函数，只能生成文本。Bridge 约定模型在需要工具时输出一个“信封”，Runtime 解析后本地执行。为兼容不同模型的输出风格，解析器支持多种形态（见 `agent-engine` 的 `parseToolCalls`）：\n\n**支持的信封格式**\n\n- `\u003Cbridge_tool>…\u003C\u002Fbridge_tool>`\n- `[[BRIDGE_TOOL]] … [[\u002FBRIDGE_TOOL]]`\n- `**Calling:** … ```` ```json ``` ````\n- `Tool: … Arguments: {…}`\n- `Action: … Action Input: {…}`\n- `【调用 xxx】{…}` 本地化形式\n- `\u003CGlob>…\u003C\u002FGlob>` 等 XML 标签\n- `\u003Ctool_call name=\"…\">`\n\n**规范信封（推荐）**\n\n````\n[[BRIDGE_TOOL]]\n{\"name\":\"Write\",\"arguments\":{\n  \"path\":\"index.html\",\n  \"content\":\"\u003C!doctype html>...\"\n}}\n[[\u002FBRIDGE_TOOL]]\n````\n\n可用工具：`Glob` `Read` `Grep` `Git_Status` `Git_Diff` `Write` `Edit`。名称大小写敏感；Write 需非空 path+content；Edit 需 path+old_string+new_string。\n\n### 工具分发表（ToolEngine.execute）\n\n| 工具名（含别名） | 底层动作 | 越界 \u002F 敏感保护 |\n| --- | --- | --- |\n| `Glob \u002F list_files \u002F list_directory` | ripgrep `--files`（失败回退 Node 递归） | 过滤 node_modules\u002F.git\u002F敏感文件，限 300 条 |\n| `Read \u002F read_file \u002F cat` | `ProjectEngine.read` | 敏感文件返回 [REDACTED]，输出限 80KB |\n| `Grep \u002F search \u002F search_files` | `ContextEngine.search`(ripgrep) | 同 Glob |\n| `Git_Status \u002F Git_Diff \u002F diff` | `GitEngine`(git) | 必须在项目根 |\n| `Write \u002F write_file \u002F propose_write` | `ToolEngine.proposeWrite` | 生成待批准变更（或直写），限 1MB |\n| `Edit \u002F replace \u002F replace_text` | `ProjectEngine.replaceText` | 精确单处匹配，否则报错 |\n\n## 7. Provider 抽象与 DeepSeek Web Provider\n\n所有 AI 网页都通过统一的 `Provider` 契约接入（定义于 `packages\u002Fprotocol`）：\n\n```ts\ninterface Provider {\n  readonly id: string;\n  readonly model: string;\n  health(): Promise\u003CProviderStatus>;\n  stream(request: ChatCompletionRequest, signal: AbortSignal): AsyncIterable\u003CChatDelta>;\n}\n```\n\n`ProviderManager` 持有已注册 Provider 的映射，提供 `active` 访问器、`switch(id)` 与 `status()`。当前仅注册 `DeepSeekWebProvider`，但切换到 ChatGPT\u002FClaude\u002FGemini 等无需改动其它模块——这正是“Provider 无关”的体现。\n\n**DeepSeekWebProvider 的工作方式**\n\n- **发 prompt**：调用 `browser.enqueuePrompt(provider, prompt)`，把消息压入命令队列，并返回异步事件流。\n- **读回答**：从扩展 POST 回来的 `BrowserEvent`（`delta` \u002F `complete` \u002F `error`）逐帧 yield 为 `ChatDelta`。\n- **超时保护**：首字超时（默认 75s）与单轮超时（默认 120s）通过 `AbortController` 中断；若页面有输出但读完无正文，提示刷新页面。\n- **健康**：`health()` 仅看扩展是否连上，不检查账号有效性。\n\n## 8. 浏览器扩展与 Runtime 通信协议\n\nRuntime 与 Chrome 扩展之间是一个 **长轮询 + 事件回传** 的 HTTP 协议（不依赖 WebSocket，部署更简单）：\n\n```mermaid\nsequenceDiagram\n  participant R as Runtime（服务端）\n  participant B as 扩展 background\n  participant C as DeepSeek 网页\n  B->>R: 1. GET \u002Fbridge\u002Fbrowser\u002Fcommands\u002Fnext（长轮询）\n  R-->>B: 2. 返回 send_prompt 命令\n  B->>C: 3. chrome.tabs.sendMessage（bridge-command）\n  B->>C: 4. 写入 composer + 点击发送\n  C-->>B: 5. 抓取回答文本（delta）\n  B->>R: 6. POST \u002Fbridge\u002Fbrowser\u002Fevents（accepted\u002Fdelta\u002Fcomplete）\n  R-->>R: 7. BrowserBridge 触发事件 → yield 给 Provider\n```\n\n扩展侧实现细节：`background.js` 以约 400ms 间隔轮询 `\u002Fbridge\u002Fbrowser\u002Fcommands\u002Fnext`，拿到命令后定位 `chat.deepseek.com` 标签页，转发给 `content.js`；`content.js` 负责在 DOM 中找到输入框（`textarea#chat-input` 或 contenteditable）、注入文本、点击发送按钮，并轮询页面直到回答稳定，再把 `delta`\u002F`complete` 事件回传。选择器集中在 `content.js`，便于在 DeepSeek 改版时单点修复。\n\n## 9. 安全模型\n\nBridge 的信任边界是 **“本地、显式授权、最小权限”**。所有文件 \u002F Git 访问都经过以下层层关卡：\n\n```mermaid\nflowchart LR\n  A[Token 鉴权] --> B[路径越界检测]\n  B --> C[敏感文件拦截]\n  C --> D[内容脱敏 \u002F 大小限制]\n  D --> E[人工审批]\n```\n\n- **路径越界保护**：`ProjectEngine.resolve` 用 `path.resolve` 后强制要求绝对路径以项目根开头，`..\u002Foutside` 直接抛错。\n- **敏感文件拦截**：`isSensitivePath` 命中 `.env*`、`*.pem\u002F*.key\u002F*.p12` 时，读取返回 `[REDACTED]`，写入直接拒绝。\n- **密钥脱敏**：`redact` 把 `api_key \u002F token \u002F secret \u002F password \u002F authorization \u002F cookie \u002F private_key \u002F jwt = …` 的值替换为 `[REDACTED]`，模型看到的是脱敏文本。\n- **写需审批**：默认 Write\u002FEdit 只生成待批准变更，需在控制台点击“批准并写入”才修改磁盘；开启 `autoApplyWrites` 的项目才会直写。\n- **尺寸上限**：单文件写入 \u002F 编辑拒绝超过 1MB 的内容。\n- **配置权限**：`~\u002F.bridge-api\u002Fconfig.json` 以 `0o600` 仅当前用户可读写；环境变量 `BRIDGE_*` 在下次启动时优先于已保存配置。\n\n## 10. 模块清单\n\n| 包 | 职责 | 关键类型 \u002F 方法 |\n| --- | --- | --- |\n| `packages\u002Fprotocol` | 跨模块类型契约 | `ChatMessage` `ChatDelta` `Provider` `BrowserCommand` `BrowserEvent` |\n| `packages\u002Fprovider-manager` | Provider 注册\u002F切换\u002F健康 | `ProviderManager.active\u002Fswitch\u002Fstatus` |\n| `packages\u002Fbrowser-bridge` | 命令队列 + 事件桥 | `BrowserBridge.enqueuePrompt\u002FwaitForCommand\u002Faccept` |\n| `packages\u002Fagent-engine` | 多轮代码代理 + 协议解析 | `AgentEngine.stream` `parseToolCalls` `validToolCall` |\n| `packages\u002Fcontext-engine` | ripgrep 检索 + 上下文构建 | `ContextEngine.search\u002Fbuild` |\n| `packages\u002Fproject-engine` | 多项目、路径保护、读写 | `ProjectEngine.add\u002Fread\u002Fwrite\u002FreplaceText\u002Ftree\u002Fresolve` |\n| `packages\u002Ftool-engine` | 工具执行、待批准变更 | `ToolEngine.execute\u002FapplyChange\u002FdiscardChange` |\n| `packages\u002Fgit-engine` | git 封装 | `GitEngine.status\u002Fdiff` |\n| `packages\u002Fsecurity-engine` | 敏感判定与脱敏 | `isSensitivePath` `redact` |\n| `packages\u002Fqueue` | 单任务串行队列 | `SingleTaskQueue.run\u002Fstatus` |\n| `packages\u002Fshared` | 通用工具 | `createId` `toErrorMessage` `expandHome` |\n| `providers\u002Fdeepseek` | DeepSeek Web Provider | `DeepSeekWebProvider.health\u002Fstream` |\n| `apps\u002Fruntime` | Runtime 入口 + HTTP 服务 + 控制台 | `createRuntimeServer` `loadConfig\u002FsaveConfig` |\n| `apps\u002Fcli` | 纯 HTTP 客户端 | `bridge doctor\\|providers\\|ask\\|search\\|diff` |\n| `apps\u002Fextension` | Chrome MV3 扩展 | `background.js` `content.js` `popup.*` |\n| `apps\u002Fnative-host` | 最小权限文件\u002FGit 代理（骨架） | 白名单 action：project.add \u002F file.read \u002F file.tree \u002F git.* |\n\n## 11. API 速查\n\n| 方法 | 路径 | 用途 |\n| --- | --- | --- |\n| `GET` | `\u002Fv1\u002Fmodels` | 列出当前模型（如 `deepseek-web`） |\n| `POST` | `\u002Fv1\u002Fchat\u002Fcompletions` | 对话补全（支持 OpenAI SSE；`bridge.mode\u002FprojectKey` 扩展字段） |\n| `GET\u002FPOST\u002FDELETE` | `\u002Fbridge\u002Fprojects` | 管理本地项目 |\n| `POST` | `\u002Fbridge\u002Fsearch` · `\u002Fbridge\u002Fcontext` | 检索 \u002F 构建上下文 |\n| `POST` | `\u002Fbridge\u002Fgit\u002Fstatus` · `\u002Fbridge\u002Fgit\u002Fdiff` | Git 信息 |\n| `POST` | `\u002Fbridge\u002Ffile\u002Fread` · `\u002Fbridge\u002Ffile\u002Ftree` | 安全读取项目文件 |\n| `GET\u002FPOST\u002FDELETE` | `\u002Fbridge\u002Fchanges` · `…\u002F:id\u002Fapply` | 查看 \u002F 批准 \u002F 丢弃待写入变更 |\n| `GET\u002FPOST` | `\u002Fbridge\u002Fproviders` · `\u002Fbridge\u002Fprovider\u002F*` | Provider 状态与切换 |\n| `GET\u002FPOST` | `\u002Fbridge\u002Fconfig` | 读取 \u002F 保存本地 Runtime 配置 |\n| `GET` | `\u002Fbridge\u002Fhealth` | Runtime 健康状态 |\n| `GET` | `\u002Fbridge\u002Fbrowser\u002Fcommands\u002Fnext` | 扩展长轮询拉取指令 |\n| `POST` | `\u002Fbridge\u002Fbrowser\u002Fevents` | 扩展回传浏览器事件 |\n\n> 鉴权：请求头 `Authorization: Bearer \u003Ctoken>` 或 `x-bridge-token: \u003Ctoken>`。代码任务可通过 `X-Bridge-Project-Key` 请求头或请求体 `bridge.projectKey` 指定跨机可移植的工作区。\n\n## 12. 运行与验证\n\n```sh\n# 安装依赖\nnpm install\n\n# 生成并导出 Bridge Token，启动 Runtime（默认 127.0.0.1:3210）\nexport BRIDGE_TOKEN=\"$(openssl rand -hex 32)\"\nnpm run dev\n\n# 打开控制台 http:\u002F\u002F127.0.0.1:3210\u002F ，输入同一 Token 即可使用图形界面\n\n# 验证\nnpm test        # node --test 单元测试（provider 切换、路径\u002F密钥防护、队列、协议）\nnpm run typecheck\n\n# CLI 示例\nbridge doctor                       # 健康检查\nbridge providers                    # 列出 Provider\nbridge ask \"解释这个项目\"            # 发起对话\nbridge search \u003Cproject> login       # 检索\nbridge diff \u003Cproject>               # Git diff\n```\n\nChrome 扩展联调：`chrome:\u002F\u002Fextensions` → 开发者模式 → 加载 `apps\u002Fextension`；粘贴 `http:\u002F\u002F127.0.0.1:3210` 与 Token，测试连接后登录 DeepSeek 网页，状态变为“DeepSeek 已连接”。","https:\u002F\u002Foxqtewbrpuiouqqjrvdv.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fpublic-media\u002F2026-07-28\u002F34426959-c183-4447-aa39-d75304c50248.jpg",[],[],{"id":18,"name":19,"slug":20,"description":21},[74,75,76,80],{"id":24,"name":25,"slug":26},{"id":32,"name":33,"slug":34},{"id":77,"name":78,"slug":79},"3e0592e0-696e-4f08-9bc4-1ff63ac83443","插件","plugin",{"id":28,"name":29,"slug":30},2,"2026-07-28T00:00:00.000Z","2026-08-05T04:12:39.799Z","2026-07-28T08:49:59.731Z",{"id":86,"type":6,"title":87,"slug":88,"summary":89,"body":90,"coverUrl":91,"productScreenshots":92,"productLinks":93,"authorName":14,"authorUrl":15,"authorSubject":16,"category":94,"tags":95,"sourceLabel":35,"sourceName":36,"sourceUrl":36,"status":37,"seoTitle":36,"seoDescription":36,"canonicalUrl":36,"isFeatured":38,"sno":103,"sortOrder":40,"publishedAt":104,"updatedAt":105,"createdAt":106},"bcd1e95f-e759-4a5a-8cdb-71f51c35f803","模型路由：为什么 AI 应用不该每个问题都调用最强模型","model-routing-quality-cost-latency","模型路由让简单请求走便宜模型，复杂或高风险任务再升级到强模型。本文解释规则路由、分类器路由和级联路由的差异，结合 RouteLLM 说明如何平衡质量、成本、延迟与风险，并给出评测和回滚清单。","## 为什么 AI 应用不该每个问题都调用最强模型\n\n一个产品通常同时面对三类请求：简单的分类和改写、中等难度的知识问答，以及需要长链路推理或复杂工具调用的任务。如果所有请求都交给最贵、最强的模型，质量可能不错，但成本和延迟会一起上升；如果全部交给小模型，简单任务很划算，难题却容易失败。\n\n模型路由（Model Routing）就是在请求进入主模型之前，先判断它适合哪一个模型。简单问题走便宜、快速的模型，困难问题才升级到更强的模型。它把“选哪个模型”从开发者写死的配置，变成了一个可以测量、调优和持续学习的系统决策。\n\n![RouteLLM 项目概念图](https:\u002F\u002Fopengraph.githubassets.com\u002F1\u002Flm-sys\u002FRouteLLM)\n\nRouteLLM 的[论文](https:\u002F\u002Farxiv.org\u002Fabs\u002F2406.18665)把问题表述成质量与成本之间的权衡，并提供了一个开源框架来训练和评估路由器。它的核心思想并不是“永远选小模型”，而是让小模型处理它擅长的请求，把强模型预算留给真正需要的地方。\n\n## 路由和负载均衡不是一回事\n\n负载均衡只关心“把请求平均分到哪台机器”，例如两台相同模型服务器各处理一半流量。\n\n模型路由关心的是“这个请求需要什么能力”：\n\n- 翻译、摘要、格式转换可以走轻量模型。\n- 复杂代码修复、数学推理和多步骤规划可能需要强模型。\n- 企业内部某类术语，可能应该路由到专门微调过的模型。\n\n所以路由器评估的不只是流量，还包括任务类型、复杂度、领域、上下文长度、用户等级和失败代价。\n\n## 三种常见路由方式\n\n### 规则路由：最容易上线\n\n可以先用明确规则：包含代码块就走代码模型，超过某个上下文长度就走长上下文模型，涉及付款就走经过安全评估的模型。\n\n规则简单、可解释、容易审计，但它通常只能看到表面特征。一个短问题也可能很难，一个很长的请求也可能只是重复文本。\n\n### 分类器路由：学习“谁更适合回答”\n\n分类器可以根据历史请求、人工偏好或评测结果，预测不同模型在当前问题上的胜率。RouteLLM 的思路就是用偏好数据训练路由器，在强模型和弱模型之间做选择。\n\n这里最重要的不是预测“这个问题难不难”，而是预测“强模型相对于弱模型能带来多少额外收益”。如果两个模型都能答对，就没有必要为了保险升级。\n\n### 级联路由：先便宜，失败再升级\n\n级联先调用小模型，再用规则、校验器或评审模型判断是否需要升级。例如结构化输出校验失败、答案缺少引用、置信度不足时，再把原问题交给强模型。\n\n它比单次路由更稳，但最坏情况下会付出两次调用的成本。对于不可接受错误的场景，级联通常比一次性押注路由器更容易解释。\n\n## 路由器到底应该看哪些信号\n\n最容易想到的信号是输入长度，但它只能说明上下文大，不代表任务难。更有用的信号通常来自四个方向：\n\n1. **任务能力**：是否需要代码、数学、翻译、视觉或长文档理解。\n2. **任务风险**：回答错了是“改写不自然”，还是会触发付款、删数据或对外发信。\n3. **结构约束**：是否要求严格 JSON、引用证据、固定字段或可执行工具参数。\n4. **业务预算**：用户等级、实时性要求、剩余配额和当前模型可用性。\n\n这些信号应当组合使用。例如，一个 100 字的“帮我修改退款金额并提交”并不长，却有明显副作用；一个 50 页的会议纪要摘要可能很长，但只需要稳定的长上下文模型，不一定需要最强推理模型。\n\n可以把决策结果理解成一个“质量—成本前沿”：在满足任务质量门槛的前提下，选择成本最低的模型；只有当便宜模型无法达到门槛时，才升级。这个原则比追求一个固定的“最强模型胜率”更适合生产系统。\n\n## 级联中的质量门槛怎么做\n\n质量门槛不能只依赖模型自己说“我有信心”。更可靠的是组合多个可验证信号：\n\n- JSON 能否通过 Schema 校验。\n- 答案是否覆盖问题里的必答字段。\n- 引用是否真的支持对应结论。\n- 工具参数是否通过类型、权限和业务规则检查。\n- 轻量评审器是否发现明显矛盾或遗漏。\n\n例如客服 Agent 先用小模型生成答案，再检查引用和退款金额。如果引用缺失，就升级到强模型；如果金额与订单 API 不一致，则直接停止自动回复。这种设计把“升级”从模糊的主观判断变成了可测试的状态机。\n\n注意质量门槛也会制造额外调用。一个评审器如果和强模型一样昂贵，路由系统就可能变成“先调用路由器，再调用评审器，再调用主模型”。因此第一层优先使用确定性校验，只有无法用规则判断的质量问题才调用评审模型。\n\n```mermaid\nflowchart TD\n    Q[\"用户请求\"] --> R[\"路由器：任务、复杂度、风险\"]\n    R --> S{\"小模型足够吗?\"}\n    S -->|是| M1[\"轻量模型\"]\n    S -->|否| M2[\"强模型或领域模型\"]\n    M1 --> V{\"通过校验或质量门槛?\"}\n    V -->|是| O[\"返回结果\"]\n    V -->|否| U[\"升级到强模型\"]\n    U --> O\n    M2 --> O\n```\n\n## 一个可落地的路由策略\n\n假设你在做一个企业知识助手，可以先定义四个等级：\n\n- **L0**：改写、分类、提取字段，使用小模型。\n- **L1**：单文档问答，使用成本较低的通用模型。\n- **L2**：跨文档比较、需要引用的问答，使用更强模型或检索级联。\n- **L3**：涉及审批、代码修改或高风险判断，使用强模型，并要求人工确认。\n\n路由器先根据请求类型和风险选择候选级别，再由质量门槛决定是否升级。这样“模型强弱”不是唯一维度，权限和失败代价也进入了决策。\n\n可以把升级条件写得很具体：\n\n- JSON Schema 校验失败。\n- 必须引用资料但没有找到证据。\n- 工具参数缺字段或类型错误。\n- 评测器判断答案没有覆盖问题中的关键约束。\n- 用户请求涉及不可逆副作用。\n\n这些条件比“感觉模型答得不自信”更容易测试。\n\n## 路由器本身也有成本\n\n如果每次请求都先调用一个昂贵的路由模型，节省下来的钱可能被路由器吃掉。路由器应该足够快、足够便宜，最好能复用已有输入特征，或使用规则与轻量分类器做第一层筛选。\n\n此外，模型能力会变化。今天的小模型可能在摘要上很好，下一次版本升级后却改变了格式习惯；供应商价格、限流和延迟也可能调整。因此路由策略不能只在上线前评估一次。\n\n## 上线要像发布一个模型一样谨慎\n\n路由器本身也会产生回归。一个新路由器可能把更多请求送进便宜模型，账单下降了，但复杂问题成功率也下降；或者路由判断准确，却因为额外网络往返让 P95 延迟变差。\n\n比较稳的发布方式是分阶段进行：\n\n1. **离线回放**：用脱敏后的真实请求和人工质量标签比较不同策略。\n2. **影子模式**：新路由器参与判断，但不改变真实模型，只记录它会怎么选。\n3. **小流量灰度**：按租户或请求类型逐步放量，监控升级率、质量和尾延迟。\n4. **自动回滚**：当高风险任务错误率、结构化失败率或人工投诉超过阈值时，切回固定模型。\n\n路由日志至少要记录最终选择、候选模型、路由理由类别、是否升级和结果质量。不要记录一段让模型自由生成的“解释”作为唯一审计依据；真正可审计的是输入特征、规则版本、阈值和最终决策。\n\n模型更换时还要重新校准。旧模型上的“0.5 阈值”没有理由在新模型上继续成立，尤其当价格、上下文能力、工具遵循能力和输出格式发生变化时。\n\n## 如何正确评估路由效果\n\n不要只看平均成本。至少要维护一个包含真实流量分布的评测集，并同时比较：\n\n1. **质量**：任务成功率、人工偏好、结构化校验、引用正确率。\n2. **成本**：平均输入输出 Token、模型调用次数、升级比例。\n3. **延迟**：P50、P95，以及升级请求的尾延迟。\n4. **安全**：高风险任务是否被正确升级，是否出现越权调用。\n5. **稳定性**：路由器换模型、换领域、换用户群后是否退化。\n\nRouteLLM 官方仓库也强调，路由阈值应使用与真实请求相近的数据进行校准，而不能直接照搬公开数据集上的阈值。因为同一个阈值，在客服、代码助手和内容审核场景里的含义完全不同。可以参考其[开源实现和评测说明](https:\u002F\u002Fgithub.com\u002Flm-sys\u002FRouteLLM)。\n\n## 常见误区\n\n**误区一：按 Token 长度判断难度。** 长文本不一定难，短问题也可能需要复杂推理。\n\n**误区二：只追求最省钱。** 如果升级率很低但错误率明显上升，节省的是账单，损失的是用户信任。\n\n**误区三：用模型评模型却不做人工抽样。** 自动评审可以扩展覆盖面，但自身也会有偏差，关键业务仍需要人工复核。\n\n**误区四：把路由器当成永久真理。** 模型、价格、流量和用户目标都会变，路由策略必须和评测、观测、回滚一起建设。\n\n**误区五：忽略失败后的用户体验。** 路由升级失败时，不能只返回“模型错误”。应该告诉用户正在重试、请求人工处理，或明确哪些部分已经完成。路由系统是产品流程的一部分，不是藏在 API 网关后面的黑盒。\n\n## 什么时候值得采用\n\n当你只有一个低流量原型时，固定使用一个模型更简单。模型路由真正有价值的信号通常是：调用量已经上升、不同任务能力差异明显、模型账单开始影响业务，或者你需要把小模型与领域模型组合起来。\n\n推荐的最小落地顺序是：先记录真实请求与质量结果，再用规则做第一版路由；确认成本和质量边界后，再训练或接入分类器；最后为高风险流程加入级联、人工确认和自动回滚。\n\n## 结语：把模型选择变成产品策略\n\n模型路由不是简单的“省钱开关”，而是一套质量、成本、延迟和风险之间的决策系统。最好的路由器不会让所有请求都走小模型，而是让每个请求得到“足够完成任务”的能力。\n\n**落地清单：**\n\n- 是否知道不同任务分别需要什么能力？\n- 是否有真实请求组成的路由评测集？\n- 是否定义了升级条件和高风险任务的硬规则？\n- 是否把路由延迟和调用成本纳入总账？\n- 是否能在质量退化时快速切回固定模型？\n\n## 一手资料\n\n- [RouteLLM 论文](https:\u002F\u002Farxiv.org\u002Fabs\u002F2406.18665)\n- [RouteLLM 开源框架](https:\u002F\u002Fgithub.com\u002Flm-sys\u002FRouteLLM)\n- [RouteLLM 的阈值校准说明](https:\u002F\u002Fgithub.com\u002Flm-sys\u002FRouteLLM#threshold-calibration)","\u002Fuploads\u002F2026-08-05\u002Ff959361f-b807-496f-8287-3e2a843e0f65.jpg",[],[],{"id":18,"name":19,"slug":20,"description":21},[96,100,101,102],{"id":97,"name":98,"slug":99},"88d2bc27-0e0f-468a-b907-2991cb97b87b","人工智能","ai",{"id":24,"name":25,"slug":26},{"id":28,"name":29,"slug":30},{"id":32,"name":33,"slug":34},66,"2026-08-05T00:00:00.000Z","2026-08-05T03:14:40.243Z","2026-08-05T02:13:46.501Z"]