跳转至

用 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 服务要在真实数据库上执行,先准备一个:

$ duckdb orders.duckdb < $DOSI_EXAMPLES/orders/seed.sql

别跳过这步

下一步不带 --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,所以注册这一步本身就是团队配置——把文件 提交上去,克隆仓库的人拿到的是同一套工具:

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 会对每个服务做健康检查,所以它告诉你的是"到底连不连得上":

$ opencode mcp list
┌  MCP Servers

●  ✓ dosi connected
│      http://127.0.0.1:8081/mcp

└  1 server(s)

走 stdio 就把 type 换成 local,由 OpenCode 自己拉起服务:不占端口、没有常驻进程、 日志走 stderr。

opencode.json
{
  "$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 两个字段。另外三个值得知道的键是 cwdenvironment,以及 timeout(毫秒,默认 5000,用于拉取工具列表)。

服务需要 token 时(--auth-token,环境变量 DOSI_SERVER_TOKEN),用 {env:VAR} 替换把密钥留在文件之外:

opencode.json
{
  "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_metriccandidates 列表,形状类错误带 suggested_retry,所以猜错只花一轮,而不是退回去手写 SQL。完整契约见 MCP 参考。还要注意每个 run_query 结果里的 row_limit_applied:结果 最终要进模型上下文,所以默认截断在 500 行(最大 5000),并且总会把截断值报出来。

第 6 步 —— 让 OpenCode 优先走指标

和某些客户端不同,面对普通的数据问题,OpenCode 不用交代就会去用指标工具。规则文件 补上的是契约的其余部分:执行前先出 SQL 预览,以及答案要报出自己的口径。没有它,同一个 问题跑的是 list_metricslist_dimensionsrun_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 可以:它是 把工具直接摘掉,而不是弹窗来问。

opencode.json
{
  "permission": { "bash": "deny", "edit": "deny" }
}

之后再要求它执行 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 那条路没了。取值是 allowaskdeny,规则支持 通配符,最后一条匹配的生效。客户端不在自己手里时,用 --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--agentOPENCODE_CONFIG,是让这类 harness 可脚本化的几个开关。

学到了什么

  • 注册:HTTP 和 stdio 两种方式都写在同一份可提交的 opencode.json 里,并用 opencode mcp list 做了健康检查。
  • token 不进仓库:用 {env:DOSI_SERVER_TOKEN} 替换,也看到客户端能区分"需要鉴权" 和"连不上"。
  • 提问:用自然语言提问,拿到按治理口径算出来的答案,对着种子数据手算核对过, 而且跑在一个又小又便宜的模型上。
  • 读轨迹:发现 → 预览 → 执行,全程没有手写 SQL;total_margin 给的是 360, 不是 270。
  • 收窄面:用 permission 限定工具,也知道了服务端的对应做法(--disable-execute)。

下一步去哪

  • 在 Agent 里使用 Dosi


    传输方式、鉴权、只读部署、完整提示词片段,以及一张排查表。

  • 接一个数仓


    把示例 DuckDB 文件换成 Postgres、Snowflake、StarRocks 等等。

  • MCP 参考


    十个工具、各自的入参形状、协议,以及错误契约。