为什么要有一个网关层
底层是 litellm,也就是说「支持哪些模型厂商」这个问题大部分被 litellm 回答了。 网关层存在不是为了再包一层,而是为了收口四件散在各处会出事的事情:
| 收口的事 | 不收口会怎样 |
|---|---|
| 模型选择 | 各节点各写默认值,改一次模型要改十几处 |
| 凭证解析 | API key 散在代码与配置里,容易进版本库 |
| token 用量 | 预算控制(见下)没有数据来源 |
| 追踪 | 每个调用点都要自己埋点,必漏 |
模型选择:三级优先
选择顺序是:命令行 --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。
这个模式是仓库「零 key、零网络、全 mock」测试约束的支点:整套 4700+ 测试 跑起来不产生任何 API 费用,也不依赖外网。没有它,CI 要么花钱要么不可靠。
工具注册表
工具不是散装函数,而是注册到一张表里,每项带一份声明: 名字、说明、参数 schema、所需权限、对哪些角色可见。
agent_loop.py:199 ToolSpec agent_loop.py:269 ToolRegistry
注册表在把工具列表交给模型之前按角色裁剪。越权的调用不会返回 「权限不足」,而是被折叠成「未知工具」——模型于是换一条路,而不是开始试探边界。 这个设计在工具治理里有展开。
生成节点实际拿到哪些工具
| 工具 | 注册条件 | 作用 |
|---|---|---|
| validate_sql | 总是 | SQL 语法校验(SQLGlot) |
| finish | 总是 | 显式收工,SQL 作为载荷交出来 |
| probe_query | standard / complex 档 | 只读试跑,看结果长什么样 |
| check_result | standard / 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;
三条与「零工具噪音」有关的分寸:三件套不常驻——常驻的只是一个约 200 字节的
catalog 发现工具,模型确认需要后才解锁,且下一轮才可见;省下的是数百 token 的常驻工具定义,
而不是能力。反过来,describe_cost 是常驻的,因为它全部的价值在于
「发生在写 SQL 之前」——藏到解锁后面等于挪到生成之后,正好错过它唯一要省的那次往返
(生成 → 被打回 → 重新生成)。
gen_sql.py:1051 build_sql_registry
后三个工具让模型能以只读方式摸到物理 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、健康检查 | 可观测性 |