首页/Agent 能力/LLM 网关

LLM 网关

全系统只有一个地方直接跟模型说话。工具治理与 agent 循环都建在它上面。这一页讲它管什么、 模型怎么选、以及为什么它要暴露三种不同的调用形态。

为什么要有一个网关层

底层是 litellm,也就是说「支持哪些模型厂商」这个问题大部分被 litellm 回答了。 网关层存在不是为了再包一层,而是为了收口四件散在各处会出事的事情:

收口的事不收口会怎样
模型选择 各节点各写默认值,改一次模型要改十几处
凭证解析 API key 散在代码与配置里,容易进版本库
token 用量 预算控制(见下)没有数据来源
追踪 每个调用点都要自己埋点,必漏

gateway.py:52 LLMGateway

模型选择:三级优先

选择顺序是:命令行 --model > 项目配置 conf/agent.yml > 用户配置 ~/.trove/conf/agent.yml。前者用于临时实验, 后者用于个人偏好,中间那个是项目约定。

配置里通常有两个模型位:一个主力、一个快速档 (target 与 model_fast)。 分档的意义是让便宜的操作不要用贵的模型——比如某些只需要判断「这是不是一句问数」的地方。

providers 与凭证

providers 是一个列表,每项可以带自定义的 api_base—— 这是接私有部署或代理网关的入口。其中的凭证字段支持 ${ENV_VAR} 占位, 在加载时展开成环境变量值。

gateway.py:99 set_providers config.py:580 resolve_env_vars

占位符不是语法糖,是防泄漏机制

写 api_key: ${DEEPSEEK_API_KEY} 而不是把 key 直接贴进 YAML, 意味着这个文件可以安全地进版本库。配置文件一旦可以被提交, 「配置漂移」这件事才有办法被 review。

providers 为空时 litellm 照常回落到环境变量(如 DEEPSEEK_API_KEY), 所以最小配置下你只需要 export 一个 key。

三种调用形态

方法用途返回
chat 最常用:给提示,要文本 字符串
chat_full 需要工具调用能力时 内容 + 工具调用 + 尽力而为的 token 用量
chat_stream 面向用户的流式输出 增量片段

gateway.py:105 chat gateway.py:187 chat_full gateway.py:277 chat_stream

chat_full 的用量是「尽力而为」的:不同厂商返回的用量字段并不统一, 拿不到时它不会报错。这一点在下游是有影响的——token 预算因此是一道软闸门, 不是精确计费器。

测试模式

网关可以带一个 mock_response 构造。设了之后,所有调用直接返回这个字符串, 不碰网络、不需要任何 key。

gateway.py:72 mock_response

这个模式是仓库「零 key、零网络、全 mock」测试约束的支点:整套 4700+ 测试 跑起来不产生任何 API 费用,也不依赖外网。没有它,CI 要么花钱要么不可靠。

工具注册表

工具不是散装函数,而是注册到一张表里,每项带一份声明: 名字、说明、参数 schema、所需权限、对哪些角色可见。

agent_loop.py:199 ToolSpec agent_loop.py:269 ToolRegistry

注册表在把工具列表交给模型之前按角色裁剪。越权的调用不会返回 「权限不足」,而是被折叠成「未知工具」——模型于是换一条路,而不是开始试探边界。 这个设计在工具治理里有展开。

agent_loop.py:627 visible_to

生成节点实际拿到哪些工具

工具注册条件作用
validate_sql总是SQL 语法校验(SQLGlot)
finish总是显式收工,SQL 作为载荷交出来
probe_querystandard / complex 档只读试跑,看结果长什么样
check_resultstandard / complex 档试跑后过规则链校验
catalog标准档以上 + analyst/admin 角色约 200 字节的发现工具:调一次,解锁下面三件套
search_values标准档以上 + analyst/admin 角色,经 catalog 解锁后下一轮可见确认某个枚举值在库里怎么写
lookup_schema同上,经 catalog 解锁懒加载表结构
explain_plan同上,经 catalog 解锁看执行计划
describe_cost标准档以上 + 预算服务在场写 SQL 之前问价:这条大概要扫多少行
load_skill存在 confirmed 的 available skill按需取 skill 正文
flowchart TB
    L1["总在
validate_sql(SQLGlot 语法校验)
finish(显式收工)"]:::exec L2["standard / complex 档
probe_query(只读试跑)· check_result(试跑 + 规则链)
预算服务在场时另有 describe_cost"]:::exec L3["以上 + analyst / admin 角色
catalog:约 200 字节的发现工具"]:::entry L4["经 catalog 解锁 · 下一轮才可见
search_values · lookup_schema · explain_plan"]:::exec L5["另一条独立条件:KB 里有 confirmed 的 available skill
load_skill"]:::entry L1 --> L2 --> L3 --> L4 L1 --> L5 classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px; classDef exec fill:#14b8a614,stroke:#14b8a699,stroke-width:1.5px;
图 1 · 工具是一级一级放出来的:门槛越高、常驻成本越低。三件套藏在 200 字节的 catalog 后面,越权调用折叠成「未知工具」而不是「权限不足」。 上面这段以源码显示:图渲染库没加载起来(离线、CDN 被拦或浏览器禁用了 JavaScript 时会这样)。这段 Mermaid 文本本身就说明了上面那些框与连线。

三条与「零工具噪音」有关的分寸:三件套不常驻——常驻的只是一个约 200 字节的 catalog 发现工具,模型确认需要后才解锁,且下一轮才可见;省下的是数百 token 的常驻工具定义, 而不是能力。反过来,describe_cost 是常驻的,因为它全部的价值在于 「发生在写 SQL 之前」——藏到解锁后面等于挪到生成之后,正好错过它唯一要省的那次往返 (生成 → 被打回 → 重新生成)。

gen_sql.py:1051 build_sql_registry

catalog 工具是「生成辅助」,不是「语义边界」

后三个工具让模型能以只读方式摸到物理 schema。它们存在的理由是: 值确认、结构懒加载、执行计划——这些确实需要看真实数据库。

但它们不参与可答边界的判定。边界由语义层说了算, 模型能看见物理表不等于它可以绕开语义模型去查。这两件事在实现上分开,是刻意的。

追踪

追踪在 agent 循环的观察者中间件里发生,不在每个调用点。 默认有一个恒在的观察者,把事件写进本地 run log;配了 Langfuse 凭证时, 同一个观察者再往 Langfuse 发一份。

agent_loop.py:454 run_agent_loop observability.py:63 langfuse_enabled observability.py:93 build_callback_handler

Langfuse 的启用条件是两个环境变量同时存在 (public key 与 secret key),配置里只有一个总开关。这样凭证缺失时是静默不启用, 而不是启动失败——追踪是增强能力,不该成为启动的前提。

trace id 由 run id 推导而来(去掉横杠),所以「按 run_id 找这一轮发生了什么」 在这两套系统里是同一个标识。详见可观测性。

observability.py:54 langfuse_trace_id

继续读

想了解去哪
循环怎么驱动、预算怎么设Agent 能力 · 生成循环
Skills 的按需加载Skills
provider 与模型配置怎么写配置参考
日志、trace、健康检查可观测性
本页源码锚点的基准提交:anchor-base: 7c6df87 GitHub 仓库 文档首页