用 OpenCode 提问¶
本页把 Dosi 注册成 OpenCode 的 MCP 服务,然后用自然语言提问, 看着 Agent 通过指标作答,而不是自己猜 SQL。所有数字都小到可以手算核对。
大约 15 分钟,会消耗少量模型额度——下面的记录跑在 DeepSeek V4 Flash 上,整页加起来 几厘钱。
前置条件
已经安装好 Dosi,有 dosi-server 可执行文件;
opencode --version 输出 1.18 或更新
(curl -fsSL https://opencode.ai/install | bash),并且配好了模型供应商;
duckdb CLI 在 PATH 上。$DOSI_EXAMPLES 指自带示例模型所在的位置——用安装
脚本的话是 ~/.local/share/dosi/examples。
用的是自带的 orders 模型:三张表、五个指标、六行数据,和
第一个指标查询那篇是同一个。
第 1 步 —— 灌一个数据库¶
MCP 服务要在真实数据库上执行,先准备一个:
别跳过这步
下一步不带 --db 的话,服务用的本地 DuckDB 是内存库,而且没灌数据。
compile_sql 照常能跑,run_query 会报表不存在——首次上手最常见的困惑。
第 2 步 —— 起 MCP 服务¶
$ dosi-server --model $DOSI_EXAMPLES/orders/model.yaml --db orders.duckdb
INFO dosi_server::bootstrap: model compiled model=.../orders/model.yaml mode="datus" datasets=3 metrics=5
INFO dosi_server: listening on http://127.0.0.1:8081
MCP 挂在 POST /mcp——顶层路径,不在 /v1 下面。先不带 Agent 验一下:
$ curl -s -X POST localhost:8081/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools | length'
10
用预编译好的二进制,别用 cargo run:在 MCP 客户端下面,一次编译会让首个工具调用
看起来像卡死。
第 3 步 —— 注册到 OpenCode¶
OpenCode 读项目根目录下的 opencode.json,所以注册这一步本身就是团队配置——把文件
提交上去,克隆仓库的人拿到的是同一套工具:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"dosi": {
"type": "remote",
"url": "http://127.0.0.1:8081/mcp",
"enabled": true
}
}
}
opencode mcp list 会对每个服务做健康检查,所以它告诉你的是"到底连不连得上":
走 stdio 就把 type 换成 local,由 OpenCode 自己拉起服务:不占端口、没有常驻进程、
日志走 stderr。
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"dosi": {
"type": "local",
"command": ["dosi-server",
"--model", "/abs/path/model.yaml",
"--db", "/abs/path/orders.duckdb",
"--mcp-stdio"]
}
}
}
command 是一个数组,不是 command / args 两个字段。另外三个值得知道的键是
cwd、environment,以及 timeout(毫秒,默认 5000,用于拉取工具列表)。
服务需要 token 时(--auth-token,环境变量 DOSI_SERVER_TOKEN),用 {env:VAR}
替换把密钥留在文件之外:
{
"mcp": {
"dosi": {
"type": "remote",
"url": "http://127.0.0.1:8081/mcp",
"headers": { "Authorization": "Bearer {env:DOSI_SERVER_TOKEN}" }
}
}
}
这正是它和手写 .mcp.json 的区别:配置可以原样提交。token 被拒和服务连不上,
opencode mcp list 分得清,一眼就能区分:
$ unset DOSI_SERVER_TOKEN && opencode mcp list
● ⚠ dosi needs authentication
│ http://127.0.0.1:8081/mcp
多份配置是合并而不是覆盖,顺序为 ~/.config/opencode/opencode.json →
$OPENCODE_CONFIG → 项目里的 opencode.json。个人全局配置和提交上去的项目配置因此
可以共存:服务写在项目文件里,个人偏好放全局。
第 4 步 —— 问第一个问题¶
交互式的话直接把问题打出来即可。这里用 headless 形式,输出可复现:
$ opencode run -m openrouter/deepseek/deepseek-v4-flash "What is revenue by order status?"
⚙ dosi_list_metrics
⚙ dosi_list_dimensions
⚙ dosi_compile_sql {"metrics":["revenue"],"group_by":[{"field":"orders.status"}]}
⚙ dosi_run_query {"metrics":["revenue"],"group_by":[{"field":"orders.status"}]}
Revenue by `orders.status`:
| status | revenue |
|-----------|--------:|
| completed | 350.00 |
| cancelled | 100.00 |
**Metric**: `revenue` — total order amount (SUM of `orders.amount`), grouped by `orders.status`.
对着种子数据核一遍:completed 是 100 + 50 + 80 + 120 = 350,cancelled 是 30 + 70 = 100。对得上——注意 Agent 还说明了它用的口径,因为口径来自模型, 不是它自己猜的。这一点值得多看一眼:这是一个又小又便宜的模型,一次就把治理口径的 答案答对了,因为它只需要挑一个指标名。
OpenCode 给 MCP 工具的命名是 <server>_<tool>——dosi_run_query,不是
mcp__dosi__run_query。
第 5 步 —— 读懂轨迹¶
--format json 会把事件流打出来,整套机制一目了然:
$ opencode run --format json -m openrouter/deepseek/deepseek-v4-flash \
"What is our total gross margin?" \
| jq -c 'select(.type=="tool_use") | .part.tool'
"dosi_list_metrics"
"dosi_describe_metric"
"dosi_compile_sql"
"dosi_run_query"
发现、确认口径、预览、执行——答案是 $360.00。这个数字本身就是语义层存在的理由:
total_margin 的定义是 SUM(orders.amount) - SUM(products.unit_cost),两个聚合
分别落在两张表上,Dosi 会先各自聚合再相减。同样的意思手写成 JOIN,得到的是 270,因为 JOIN 把每个商品按订单
数重复了一遍,成本侧被放大。推导过程在
Claude Code 第 6 步。
错误是刻意做成结构化的:unknown_metric 带 candidates 列表,形状类错误带
suggested_retry,所以猜错只花一轮,而不是退回去手写 SQL。完整契约见
MCP 参考。还要注意每个 run_query 结果里的 row_limit_applied:结果
最终要进模型上下文,所以默认截断在 500 行(最大 5000),并且总会把截断值报出来。
第 6 步 —— 让 OpenCode 优先走指标¶
和某些客户端不同,面对普通的数据问题,OpenCode 不用交代就会去用指标工具。规则文件
补上的是契约的其余部分:执行前先出 SQL 预览,以及答案要报出自己的口径。没有它,同一个
问题跑的是 list_metrics → list_dimensions → run_query,只打一张干巴巴的表;
有它,中间会多一次 compile_sql,答案也带上了出处。
把这段放进项目的 AGENTS.md:
## Data questions
Answer data questions through the `dosi` MCP tools, never by writing SQL
against raw tables.
1. Discover: `list_metrics`, then `list_dimensions` for the breakdown fields.
2. Preview: `compile_sql` before executing, and show me the SQL.
3. Execute: `run_query`.
Never invent a metric name — read the error's `candidates` and retry. Never
re-derive a metric by hand. If no metric covers the question, say so.
完整版本连同每条规则的理由,在在 Agent 里使用 Dosi。
OpenCode 从项目根目录开始沿目录树向上读 AGENTS.md,然后是
~/.config/opencode/AGENTS.md,最后回退到 CLAUDE.md——所以已经按
Claude Code 那篇配过的仓库,不用再写第二份规则文件。
第 7 步 —— 收窄工具面¶
光有工具,拦不住一个同时还有 shell 的 Agent 自己去手写 SQL。permission 可以:它是
把工具直接摘掉,而不是弹窗来问。
之后再要求它执行 shell 命令,模型会被明确告知自己手上到底有什么:
$ opencode run -m openrouter/deepseek/deepseek-v4-flash \
"Run 'echo hello' in the shell, then tell me revenue by order status."
✗ Invalid Tool
Model tried to call unavailable tool 'bash'. Available tools: dosi_compile_sql,
dosi_describe_metric, dosi_explain_query, dosi_get_capabilities,
dosi_list_connections, dosi_list_datasets, dosi_list_dimensions,
dosi_list_metrics, dosi_run_query, dosi_validate_model, glob, grep, read, ...
⚙ dosi_list_metrics
⚙ dosi_list_dimensions
⚙ dosi_compile_sql
⚙ dosi_run_query
I can't run shell commands in this environment (no bash tool available), but
here's the revenue data:
| status | revenue |
|-----------|--------:|
| completed | 350.0 |
| cancelled | 100.0 |
指标这条路照样走得通,手写 SQL 那条路没了。取值是 allow、ask、deny,规则支持
通配符,最后一条匹配的生效。客户端不在自己手里时,用 --disable-execute 在服务端
强制同样的规则。
第 8 步 —— 自己量一遍¶
每个 step_finish 事件都带着 token 数和成本:
$ opencode run --format json -m openrouter/deepseek/deepseek-v4-flash \
"What is our total gross margin?" \
| jq -s '{tools: [.[] | select(.type=="tool_use") | .part.tool],
cost: ([.[] | select(.part.cost != null) | .part.cost] | add)}'
{
"tools": ["dosi_list_metrics", "dosi_describe_metric", "dosi_compile_sql", "dosi_run_query"],
"cost": 0.0019781874
}
(你的数字会不一样——模型、机器、问法都会影响。)
这就是和"裸表结构 NL2SQL"做 A/B 的原始材料:同一批问题、同一个库,一组给 MCP 工具,
另一组只给 SQL shell,然后比正确率、轮数和 token。我们自己的那一份——连基线提示词
一起公开,方便核对这个对比公不公平——在基准测试。预期要摆正:在
这个 fixture 上,指标路线赢在正确率,输在上下文体积,因为十个工具定义比三张表
的 schema dump 更贵,见代价在哪里。--session、--agent
和 OPENCODE_CONFIG,是让这类 harness 可脚本化的几个开关。
学到了什么¶
- 注册:HTTP 和 stdio 两种方式都写在同一份可提交的
opencode.json里,并用opencode mcp list做了健康检查。 - token 不进仓库:用
{env:DOSI_SERVER_TOKEN}替换,也看到客户端能区分"需要鉴权" 和"连不上"。 - 提问:用自然语言提问,拿到按治理口径算出来的答案,对着种子数据手算核对过, 而且跑在一个又小又便宜的模型上。
- 读轨迹:发现 → 预览 → 执行,全程没有手写 SQL;
total_margin给的是 360, 不是 270。 - 收窄面:用
permission限定工具,也知道了服务端的对应做法(--disable-execute)。
下一步去哪¶
-
传输方式、鉴权、只读部署、完整提示词片段,以及一张排查表。
-
把示例 DuckDB 文件换成 Postgres、Snowflake、StarRocks 等等。
-
十个工具、各自的入参形状、协议,以及错误契约。