返回

MCP Toolbox:如何将Agent文本转为ClickHouse向量

RSS ClickHouse Blog AI 逐段翻译 发布 2026-09-07 17:00 收录于 09-08

有效阅读仅保存在当前浏览器,用于「为你」推荐;暂停或清除记录。

DataHot 速览

Google 开源 MCP Toolbox for Databases 将 Agent 文本与数据库向量检索之间的转换内置到工具中,你只需在 YAML 中声明 Gemini embedding 模型,插入和搜索时嵌入与 ClickHouse 排序即可透明完成,无需自建嵌入服务。文章以 Toolbox 1.9.0 和 ClickHouse Cloud 26.4.1 为例,演示了从安装配置到自定义 ingestion/search 工具、再到加载语料验证结果的完整链路。

为什么值得关注:该实操展示了一种可复用的 Agent 语义检索构建方式,减少手写文本转 SQL/向量的胶水代码,对 Data Agent 工程落地有直接参考价值。

本文目录 14 节
  1. 什么是MCP Toolbox?
  2. 安装
  3. 快速开始:预构建的ClickHouse工具
  4. 原生文本 → 向量 → 搜索
  5. 第1步:ClickHouse表
  6. 第2步:Toolbox配置
  7. 第3步:运行它
  8. 加载语料库
  9. 搜索它
  10. 使用它:MCP客户端和应用SDK
  11. 从MCP客户端(Claude Code,Gemini CLI等)
  12. 从应用程序代码
  13. 生产注意事项
  14. 总结

译文

AI 逐段翻译

如果你构建了一个需要语义搜索的AI代理,你可能也遇到了每个人都遇到的那个尴尬的差距:你的LLM说文本,你的数据库说SQL和向量,而中间必须有东西来做翻译。通常那个东西最终变成你编写和维护的定制应用程序代码或工具——接受查询字符串,调用嵌入API,格式化向量,将其拼接进SQL,希望你能正确转义所有内容。

MCP Toolbox for Databases是Google的产品,原生地弥合了这一差距,并且它可以与ClickHouse开箱即用地配合。你在YAML中声明一个Gemini嵌入模型,将工具参数标记为embeddedBy那个模型,Toolbox就会透明地处理整个文本 → 向量 → 搜索管道。代理永远看不到向量。它发送"如何在表上配置TTL?"并得到排好序的行。

在这篇文章中,我将介绍什么是MCP Toolbox,如何安装和设置它,如何使用其预构建的ClickHouse工具,然后重点介绍:构建一个在插入时嵌入的摄取工具和一个嵌入查询并按余弦距离排序的搜索工具——无需自己编写或维护嵌入服务。然后我们将通过它加载一个合成语料库,看看实际返回的结果。

想尝试一下吗?把这篇博文交给你的编码代理,让它按照步骤为你设置一切。

以下所有内容都是针对Toolbox 1.9.0和ClickHouse Cloud 26.4.1服务端到端运行的,这是撰写本文时的稳定版本。

什么是MCP Toolbox?

MCP Toolbox for Databases是Google的开源(Apache 2.0)模型上下文协议服务器,最初作为genai-toolbox在MCP出现之前发布,之后更名。它是一个单一的Go二进制文件,位于AI代理和你的数据库之间,服务于两个不同的目的:

  1. 一个现成的MCP服务器。使用--prebuilt标志将其指向ClickHouse和Postgres等数据库,任何MCP客户端——Claude Code、Gemini CLI、Codex、你的IDE——会立即获得像execute_sql和list_tables这样的通用工具。非常适合探索和开发。
  2. 一个自定义工具框架。在YAML中定义精心设计的参数化SQL语句,并将这些暴露为工具,而不是原始SQL访问。这对于生产环境来说是一个很好的模式,因为代理只能调用你编写的查询,传递驱动程序在到达数据库时转义的参数。

除了ClickHouse,它还支持PostgreSQL、MySQL、SQL Server、Oracle、MongoDB、Redis、Valkey、Elasticsearch、Neo4j、Cassandra、Snowflake、Trino、CockroachDB、TiDB以及Google Cloud系列(AlloyDB、BigQuery、Cloud SQL、Spanner、Firestore)。单个tools.yaml可以定义跨其中几个的数据源,所以一个MCP端点可以并排为你的ClickHouse分析和Postgres应用数据库暴露工具。然而,这不是联邦——每个工具恰好绑定到一个数据源,因此代理通过两次调用并在其自身上下文中连接结果来关联两者,而不是在SQL中。

在底层,你还免费获得连接池、可选的经过身份验证的工具调用以及OpenTelemetry指标和追踪。

安装

选择适合你的设置的任何一种:

1# Homebrew (macOS / Linux)2brew install mcp-toolbox34# Or grab the binary directly (see the releases page for versions/platforms)5export VERSION=1.9.06curl -L -o toolbox https://storage.googleapis.com/mcp-toolbox-for-databases/v$VERSION/darwin/arm64/toolbox7chmod +x toolbox89# Or Docker10docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION1112# Or zero-install via npx (convenient, but not the fastest startup)13npx @toolbox-sdk/server --config tools.yaml

用toolbox --version验证。服务器默认监听127.0.0.1:5000——回环地址,不是所有接口,这对于持有数据库凭据的进程来说是恰当的默认设置。这里是一个示例tools.yaml

1kind: source2name: my-clickhouse3type: clickhouse4host: ${CLICKHOUSE_HOST}5port: ${CLICKHOUSE_PORT}6database: ${CLICKHOUSE_DATABASE}7user: ${CLICKHOUSE_USER}8password: ${CLICKHOUSE_PASSWORD}9protocol: ${CLICKHOUSE_PROTOCOL}10secure: true1112---13kind: tool14name: execute_sql15type: clickhouse-execute-sql16source: my-clickhouse17description: Execute a SQL query against ClickHouse and return the rows.1819---20kind: tool21name: list_databases22type: clickhouse-list-databases23source: my-clickhouse24description: List all databases in ClickHouse.2526---27kind: tool28name: list_tables29type: clickhouse-list-tables30source: my-clickhouse31description: List the tables in a ClickHouse database.3233---34kind: embeddingModel35name: gemini-embedder36type: gemini37model: gemini-embedding-00138project: ${GOOGLE_CLOUD_PROJECT}39location: ${GOOGLE_CLOUD_LOCATION}40dimension: 7684142---43kind: tool44name: insert_doc45type: clickhouse-sql46source: my-clickhouse47description: Indexes a new document and its vector embedding.48statement: |49  INSERT INTO vectors.documents (content, embedding) VALUES (?, ?)50parameters:51  - name: content52    type: string53    description: The text content to store.54  - name: text_to_embed55    type: string56    description: Hidden copy of content, embedded as a vector.57    valueFromParam: content58    embeddedBy: gemini-embedder5960---61kind: tool62name: search_docs63type: clickhouse-sql64source: my-clickhouse65description: Finds the most semantically similar documents to a query.66statement: |67  SELECT content, cosineDistance(embedding, ?) AS distance68  FROM vectors.documents69  ORDER BY distance ASC70  LIMIT 571parameters:72  - name: query73    type: string74    description: The natural-language search query.75    embeddedBy: gemini-embedder7677---78kind: toolset79name: semantic_search80tools:81  - insert_doc82  - search_docs8384---85kind: toolset86name: clickhouse_explore87tools:88  - execute_sql89  - list_databases90  - list_tables

在编写任何YAML之前,提醒一下:1.9偏好扁平配置格式。每个资源是它自己的YAML文档,带有kind、name和type键,用---分隔。互联网上的旧示例使用嵌套格式(sources: → my-clickhouse: → …)其中kind携带类型。那种旧格式在1.9中仍然可以解析并正常运行(我保留了一个嵌套配置,它很愉快地执行了),所以如果你有旧的配置,也不会坏。toolbox migrate当你想要新格式时转换它,下面的所有示例都使用新格式。

快速开始:预构建的ClickHouse工具

你可以将Toolbox用作通用的ClickHouse MCP服务器,类似于将它连接到mcp-clickhouse或ClickHouse Cloud MCP服务器。将此添加到你的MCP客户端配置中(例如,.mcp.json适用于Claude Code或claude_desktop_config.json适用于Claude Desktop):

1{2"mcpServers":{3"clickhouse":{4"command":"npx",5"args":["-y","@toolbox-sdk/server","--prebuilt=clickhouse","--stdio"],6"env":{7"CLICKHOUSE_HOST":"your-instance.clickhouse.cloud",8"CLICKHOUSE_PORT":"8443",9"CLICKHOUSE_USER":"default",10"CLICKHOUSE_PASSWORD":"…",11"CLICKHOUSE_DATABASE":"default",12"CLICKHOUSE_PROTOCOL":"https"13}14}15}16}

所有这六个变量都是必需的,并且预构建配置会快速失败,并确切告诉你缺少哪一个,这比连接超时好。请注意,CLICKHOUSE_HOST是一个裸主机名:scheme和端口位于CLICKHOUSE_PROTOCOL和CLICKHOUSE_PORT中,所以https://host:8443在主机字段中不起作用。

这立即给代理提供了三个工具:execute_sql、list_databases和list_tables。你现在可以问“我的events表的schema是什么?”代理会弄清楚。

Toolbox对于这种模式是做什么的非常直接,每次启动时都会记录一条警告:

这些预构建配置旨在用于‘构建时’用例,即代理在帮助受信任的开发人员构建东西。它们对于‘运行时’用例不够安全,即代理将可能面对不受信任的开发人员。

这就是下面策划的工具的暗示。

原生文本 → 向量 → 搜索

ClickHouse也支持强大的向量搜索,并包含余弦等距离函数和对HNSW索引的支持。Toolbox的自定义工具框架将嵌入模型作为一等资源类型。当工具参数带有embeddedBy: <model-name>提示时,Toolbox在调用时拦截原始文本,将其批量发送到嵌入模型的API,并将生成的向量绑定到语句的?占位符。

对于ClickHouse,该向量被传递给clickhouse-go驱动,作为原始[]float32而不是你自己组装的字符串:

1// internal/embeddingmodels/embeddingmodels.go2// FormatVectorForClickHouse returns the raw []float32 slice, which the3// clickhouse-go driver binds natively to Array(Float32) parameters.4funcFormatVectorForClickHouse(vectorFloats []float32) any {5iflen(vectorFloats) == 0 {6return []float32{}7    }8return vectorFloats9}

这意味着你的SQL可以直接针对参数占位符使用ClickHouse的向量函数。代码库中恰好有两个这样的格式化器——一个用于pgvector,一个用于ClickHouse。

不要被?困惑,它和“预编译语句”不一样。我从system.query_log中取回执行的语句,向量以字面量内联到达:

1query_head: SELECT content, cosineDistance(embedding, [-0.002279004, 0.012003845, 0.0052243723, …2query_len:  106263param_1 present in Settings: 0

这里没有服务器端参数绑定,也没有param_*设置,只是一个十千字节的SQL字符串,由clickhouse-go在客户端插值。我测试了一些基本的注入,看看这对安全性的影响,通过将x') AS a FROM system.one WHERE 1=1 UNION ALL SELECT version(--放入一个string参数。它作为结果列中的精确文本返回,而不是作为执行的SQL。所以保证是驱动程序为你序列化类型值并转义它们,这消除了你手动构建字面量时可能创建的注入类。只是它不是预处理语句。

它也有缺点。每个嵌入式查询都会将完整的文本(包括向量)写入query_log。繁忙的搜索工具会大幅增加该表的大小,而向量是你能放在那里最不易压缩的东西。

第1步:ClickHouse表

我们需要一个表来保存文档及其嵌入。这里有一些刻意的选择:

1CREATE DATABASE vectors;23CREATE TABLE vectors.documents4(5    id        UUID DEFAULT generateUUIDv4(),6    content   String,7    embedding Array(Float32),8    INDEX idx_embedding embedding TYPE vector_similarity('hnsw', 'cosineDistance', 768)9)10ENGINE = MergeTree11ORDERBY (id);

关于模式的说明:

  • Array(Float32)是ClickHouse中规范的嵌入列类型,也是Toolbox绑定的类型。
  • 该vector_similarity跳过索引提供近似最近邻(HNSW)搜索。第三个参数(768)是向量维度,并且必须与你在嵌入模型上配置的维度匹配。在26.4上,allow_experimental_vector_similarity_index和enable_vector_similarity_index都已经是11,所以无需设置更改;在旧版本上你可能需要自己启用它们。对于小型语料库(最多几百万行),你可以完全跳过索引——暴力cosineDistance在列式Array(Float32)上很快,你以后随时可以用ALTER TABLE ... ADD INDEX添加索引。
  • 选择余弦并坚持使用。 gemini-embedding-001在768维度的输出不是单位归一化的——在我的语料库中,L2范数在0.5788到0.5928之间,平均0.5862。cosineDistance在内部归一化,所以它不在乎,但L2Distance会给出不同的排名,并随幅度偏移,并且由于上面的索引声明了'cosineDistance',一个L2Distance查询完全得不到索引帮助。选择一个度量,在索引中声明它,并一致地使用它。
  • ORDER BY不会加速向量搜索本身——ANN搜索通过跳过索引进行。根据你将与向量搜索结合使用的元数据过滤器(租户、类别、日期)来选择你的ORDER BY。它实际上是不可变的,ALTER TABLE … MODIFY ORDER BY只能追加列,不能重新排序或删除它们,所以在加载数据之前决定。如果你的搜索工具总是按,例如,category过滤,把它放在第一位:ORDER BY (category, id)。

在ClickHouse Cloud上,你会自动获得共享存储引擎,索引完好地保留:

1CREATE TABLE vectors.documents2(3    `id` UUID DEFAULT generateUUIDv4(),4    `content` String,5    `embedding` Array(Float32),6    INDEX idx_embedding embedding TYPE vector_similarity('hnsw', 'cosineDistance', 768) GRANULARITY 1000000007)8ENGINE = SharedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')9ORDERBY id10SETTINGS index_granularity =8192

第2步:Toolbox配置

一切都在一个tools.yaml中。三种资源:一个源、一个嵌入模型和工具。

源 ClickHouse Cloud通过8443端口上的HTTPS连接;本地实例使用8123端口上的HTTP:

1kind: source2name: my-clickhouse3type: clickhouse4host: ${CLICKHOUSE_HOST}5port: ${CLICKHOUSE_PORT}6database: ${CLICKHOUSE_DATABASE}7user: ${CLICKHOUSE_USER}8password: ${CLICKHOUSE_PASSWORD}9protocol: ${CLICKHOUSE_PROTOCOL}10secure: true

嵌入模型 截至1.9,gemini是唯一支持的提供商——在internal/embeddingmodels下有一个gemini包,而openai、ollama、bedrock等都在配置解析时被拒绝。如果你今天需要不同的提供商,你必须在Toolbox之外进行嵌入。

有两种认证方式,选择由你设置哪些字段决定。来自Google AI Studio的API密钥:

1kind: embeddingModel2name: gemini-embedder3type: gemini4model: gemini-embedding-0015apiKey: ${GOOGLE_API_KEY}6dimension: 768

或者project和location而不是apiKey,这会切换到Vertex AI后端并获取你的应用程序默认凭据:

1kind: embeddingModel2name: gemini-embedder3type: gemini4model: gemini-embedding-0015project: ${GOOGLE_CLOUD_PROJECT}6location: ${GOOGLE_CLOUD_LOCATION}   # e.g. us-central17dimension: 768

如果已经完成gcloud auth application-default login,第二种形式无需进一步设置即可工作,服务器在启动时确认它走了哪条路径:

1INFO "Using Vertex AI backend for Gemini embedding" "my-project" "us-central1"2INFO "Initialized 1 embeddingModels: gemini-embedder"

字段列表简短而严格:model、dimension,然后要么是apiKey要么是project加location。看起来合理的额外字段(useVertex、taskType、outputDimensionality)都被拒绝为未知字段。你可以在源代码中看到拒绝这些字段的原因:

1// internal/embeddingmodels/gemini/gemini.go2embedConfig := &genai.EmbedContentConfig{3    TaskType: "SEMANTIC_SIMILARITY",4}56if m.Dimension > 0 {7    embedConfig.OutputDimensionality = genai.Ptr(m.Dimension)8}

首先,dimension直接映射到OutputDimensionality,这告诉你该字段指示模型而不是描述它。针对相同的gemini-embedding-001设置dimension: 512你会得到一个512元素的向量。所以没有需要查找的模型输出维度;dimension唯一需要同意的是列中声明的宽度vector_similarity索引。因为Toolbox不知道那是什么,不一致会在插入或搜索时以ClickHouse错误的形式出现,而不是配置解析失败。一开始就做对。

第二,对于语义搜索文章更重要的是:taskType被硬编码为SEMANTIC_SIMILARITY,这就是为什么配置字段被拒绝。谷歌自己的检索指导是不对称的——将你的文档嵌入为RETRIEVAL_DOCUMENT,将你的查询嵌入为RETRIEVAL_QUERY,这样两侧落在模型调优的空间中。Toolbox两者都不提供,也没有覆盖。SEMANTIC_SIMILARITY优化“这两段文本相似吗?”,这与“这个文档回答了这个问题吗?”略有不同,并且在摄取和搜索路径上是相同的任务类型。

在实践中它仍然工作良好,如下面的结果所示。但它是你将在那里看到的压缩距离分布的一个可能促成因素——所有内容落在0.15到0.30之间,而不是分散开——如果你需要挤压检索质量,这是你首先会尝试的旋钮,也是你目前无法转动的。今天在Toolbox之外嵌入是设置它的唯一方法。

摄取工具 这是巧妙的部分。在插入时,你需要相同的文本两次——一次存储为String,一次嵌入到向量列。要求LLM跨两个参数重复相同的字符串是浪费且容易出错的,所以Toolbox有valueFromParam:一个隐藏参数,镜像另一个参数的值。它永远不会出现在工具清单中——代理不知道它存在:

1kind: tool2name: insert_doc3type: clickhouse-sql4source: my-clickhouse5description: Indexes a new document and its vector embedding.6statement: |7  INSERT INTO vectors.documents (content, embedding) VALUES (?, ?)8parameters:9  - name: content10    type: string11    description: The text content to store.12  - name: text_to_embed13    type: string14    description: Hidden copy of content, embedded as a vector.15    valueFromParam: content16    embeddedBy: gemini-embedder

通过MCP向服务器询问其工具清单,返回insert_doc的单个属性:

1- execute_sql    | params: sql2- insert_doc     | params: content3- list_databases | params: none4- list_tables    | params: database5- search_docs    | params: query

搜索工具。代理提供纯文本;Toolbox 将其嵌入并绑定向量到?:

1kind: tool2name: search_docs3type: clickhouse-sql4source: my-clickhouse5description: Finds the most semantically similar documents to a query.6statement: |7  SELECT content, cosineDistance(embedding, ?) AS distance8  FROM vectors.documents9  ORDER BY distance ASC10  LIMIT 511parameters:12  - name: query13    type: string14    description: The natural-language search query.15    embeddedBy: gemini-embedder

可选地,将它们分组为一个工具集,以便客户端仅加载这两个:

1kind: toolset2name: semantic_search3tools:4  - insert_doc5  - search_docs

要记住两个约束。只有字符串类型的参数可以声明embeddedBy,配置解析器在服务器启动前就能捕获这一点:

1ERROR "unable to parse config file at \"tools.yaml\": document 3: error unmarshaling2tool \"dim_probe\": … parameter type \"integer\" cannot specify 'embeddedBy'"

引用的嵌入模型也必须定义在同一配置中——但该点在解析时不检查。指向不存在模型的工具能顺利加载,并在首次使用时失败,这是一个更糟的发现问题的地方:

1ERROR "error embedding parameters: embedding model does not exist: does-not-exist"

第3步:运行它

1./toolbox --config tools.yaml   # listens on 127.0.0.1:5000

我通常直接去捆绑的UI,位于http://127.0.0.1:5000/ui(添加--ui当你启动服务器时)来目测加载了什么。但在连接客户端之前,你可以直接从shell测试工具:

1toolbox invoke search_docs '{"query":"making queries faster"}' \2  --config tools.yaml --log-level ERROR

的--log-level ERROR如果管道传输到jq,这很重要:Toolbox将其启动日志行写入stdout,与JSON结果混在一起,因此默认级别的调用产生非有效JSON的输出。这个单行是证明你的嵌入凭据有效的最快方式,然后再调试客户端集成。

加载语料库

一个两行的演示并不能告诉你语义搜索是否有效,所以我们可以加载一些主题分布足够广泛的内容,以便可证伪:57个短文档,分布在五个簇中——ClickHouse内部、运维和Kubernetes、通用编程、烹饪和山地徒步。真正语义化的检索应该在措辞要求时跨簇边界,并且在不该跨时保持在内。

摄取通过insert_doc工具本身进行,因此所有嵌入都由Toolbox完成。注意,在1.9中,/api/tool/<name>/invokeREST端点默认是关闭的,没有该标志命中端点会返回410 Gone,并带有错误:

1{"status":"Gone","error":"/api native endpoints are disabled by default. Please use the standard /mcp JSON-RPC endpoint"}

用--enable-api启动服务器,同样的请求返回200,结果排名正确,因此如果你想要更简单的客户端,REST路由可用。我选择使用MCP JSON-RPC,原因有二:它不需要额外标志,而且它是你的代理会使用的传输方式,所以种子路径与生产流量使用相同的代码。

整个客户端大约二十行:

1definvoke(url: str, tool: str, arguments: dict) -> dict:2"""Call a tool over Toolbox's MCP JSON-RPC endpoint."""3    payload = {4"jsonrpc": "2.0",5"id": 1,6"method": "tools/call",7"params": {"name": tool, "arguments": arguments},8    }9    request = urllib.request.Request(10f"{url}/mcp",11        data=json.dumps(payload).encode(),12        headers={13"Content-Type": "application/json",14"Accept": "application/json, text/event-stream",15        },16    )17with urllib.request.urlopen(request) as response:18        body = json.load(response)19if"error"in body:20raise RuntimeError(body["error"]["message"])21return body["result"]

的Accept: application/json, text/event-stream头是流式HTTP MCP传输规范指定的。Toolbox 1.9即使没有它也能响应,但无论如何发送它——它不花费任何东西,并且这是符合规范的客户端会做的。

然后将语料库分发到一个小线程池中:

1with ThreadPoolExecutor(max_workers=8) as pool:2    failures = [failure for failure in pool.map(insert, documents) if failure]
1$ python3 seed_documents.py --url http://127.0.0.1:50002inserted 57/57 in 4.1s

57个文档用时4秒,并发8,每次嵌入往返Vertex AI。确认哪些已加载:

1SELECTcount() AS docs, any(length(embedding)) AS dims2FROM vectors.documents
1┌─docs─┬─dims─┐21. │   57 │  768 │3   └──────┴──────┘

以及存储成本,这比听起来更有趣。但要注意询问哪些列:data_uncompressed_bytes仅覆盖列数据,而bytes_on_disk包括二级索引,所以直接比较这两个不会告诉你任何信息。单独分解索引:

1SELECTsum(rows) AS docs,2       formatReadableSize(sum(data_uncompressed_bytes))              AS col_uncompressed,3       formatReadableSize(sum(data_compressed_bytes))                AS col_compressed,4       formatReadableSize(sum(secondary_indices_uncompressed_bytes)) AS idx_uncompressed,5       formatReadableSize(sum(secondary_indices_compressed_bytes))   AS idx_compressed,6       formatReadableSize(sum(bytes_on_disk))                        AS on_disk7FROM system.parts8WHERE database ='vectors'ANDtable='documents'AND active
1┌─docs─┬─col_uncompressed─┬─col_compressed─┬─idx_uncompressed─┬─idx_compressed─┬─on_disk────┐21. │   57 │ 178.36 KiB       │ 162.34 KiB     │ 100.77 KiB       │ 69.05 KiB      │ 231.85 KiB │3   └──────┴──────────────────┴────────────────┴──────────────────┴────────────────┴────────────┘

嵌入确实几乎不可压缩——178.36 KiB降至162.34 KiB是压缩比1.1x,而日志和度量列通常能达到10倍或更多,因为高熵浮点数让编解码器没有可操作的空间。(该数字涵盖所有三列,但embedding占大部分;content是总共的几KiB。)而HNSW索引在此规模下是分区的29.8%,在232 KiB总数中占69 KiB,对于暴力扫描57行来说没有带来任何优势。所以如果你问“小语料库是否应该添加索引?”:目前还不行。

搜索它

现在是成功与否的部分。四个查询,没有一个与它们应检索的文档共享有意义的关键词,每个查询返回前三个结果,并附带余弦距离:

"使查询更快"

10.1653  Distributed tables fan a query out to shards and merge the partial results…20.1708  Dictionaries hold reference data in memory so joins become key lookups…30.1715  Reading fewer columns is the single biggest lever on scan performance…

"为什么我的容器被杀"

10.1833  The OOMKilled reason on a terminated container means it exceeded its memory limit…20.2313  Liveness probes restart a container; readiness probes only remove it from…30.2345  A pod stuck in CrashLoopBackOff is usually failing its own startup…

"在山上恶劣天气中保持安全"

10.1565  Pitching a tent on a slight rise keeps you dry when overnight rain pools…20.1667  Mountain weather can turn within an hour, so carry a shell even on a clear morning.30.2069  Navigating with a map and compass still works when the phone battery dies…

第二个是我最喜欢的:“为什么我的容器被杀”首先检索到OOMKilled说明,除了单词killed和container之外,查询与其没有任何共同点——没有“memory”,没有“limit”。所有三个命中都留在十二个文档的运维簇内,其他45个文档没有打扰。

然后是不太令人印象深刻的那个:

"我晚餐应该做什么"

10.2853  Sourdough starter needs feeding with equal parts flour and water roughly…20.2943  Baking is closer to chemistry than cooking, so weigh your flour rather than…30.3030  Distributed traces show where a request spent its time; metrics tell you…

前两个在正确的簇中,但一个分布式追踪笔记在排名第三位闯入了晚餐聚会。整个结果集在0.28到0.30之间,比好查询的0.16-0.23要平坦和遥远得多。我的语料库中根本没有回答“我晚餐应该做什么”的文档——它有烹饪技巧,而不是食谱——而纯top-k搜索总是返回k行,无论它们是否应存在。在生产中,你会应用距离阈值(WHERE distance < 0.25,根据你自己的数据调整),以便工具返回空而不是返回噪音。代理拿到三个无关行通常会尝试使用它们。

使用它:MCP客户端和应用SDK

从MCP客户端(Claude Code,Gemini CLI等)

将你的客户端指向运行中的HTTP服务器:

1{2"mcpServers":{3"clickhouse-semantic-search":{4"type":"http",5"url":"http://127.0.0.1:5000/mcp/semantic_search"6}7}8}

http://127.0.0.1:5000/mcp暴露文件中的每个工具;附加工具集名称将其限定到该工具集。

如果你不想管理长期运行的服务器,让客户端通过stdio拥有进程。此形式也解决了凭据加载问题,因为MCP客户端不会获取你的shell配置文件:

1{2"mcpServers":{3"clickhouse-toolbox":{4"command":"sh",5"args":[6"-c",7"set -a; . \"$HOME/.config/clickhouse.env\"; set +a; exec toolbox --config /path/to/tools.yaml --stdio"8]9}10}11}

一个坑花费了我比应该更多的时间,真正原因不是显而易见的。我将嵌入模型和向量工具添加到tools.yaml,而一个已运行的Toolbox进程继续提供旧的三个工具清单。tools/list不包括insert_doc,而无论如何调用它返回invalid tool name: tool with name "insert_doc" does not exist——这让你寻找不存在的YAML错误。

我最初的假设是热重载失败了。但它没有:重载工作正常,无论是原地追加还是原子替换,都在一两秒内。真正的罪魁祸首是${VAR}插值解析针对运行进程的环境。那个服务器是在我添加之前启动的GOOGLE_CLOUD_PROJECT和GOOGLE_CLOUD_LOCATION 添加到我的 env 文件中,所以每次重新加载尝试都遇到无法解析的变量,而 Toolbox 合理地保留了最后一次有效的配置。难以发现的原因是日志级别:

1WARN "error loading configs unable to parse config file at \"tools.yaml\":2error parsing environment variables: environment variable not found: \"GOOGLE_CLOUD_PROJECT\""

那是WARN,不是ERROR,埋在请求日志里,在一个你可能一小时前在另一个终端启动的服务器上。所以:热重载是真的,你可以依赖它进行语句和参数更改,但任何引入新环境变量的编辑都需要重启,因为正在运行的进程无法看到其分支时并不存在的变量。

现在从代理的角度看,交互是这样的:

用户:保存这条笔记:“ClickHouse 跳过索引存储关于 granule 的元数据,以便查询可以跳过肯定不匹配的块。”代理: 调用insert_doc(content="ClickHouse 跳过索引存储…") - Toolbox 将内容复制到隐藏参数中,将其嵌入 768 维向量,并将两者绑定到 INSERT。用户:我保存了什么关于加速查询的内容?代理: 调用search_docs(query="加速查询") - Toolbox 嵌入查询,ClickHouse 按余弦距离排序。

代理的工具调用和数据库查询都是纯文本和纯 SQL。向量层是不可见的。

从应用程序代码

对于自定义代理,Toolbox 提供 Python、JavaScript/TypeScript、Go 和 Java 的客户端 SDK,以及 LangChain/LangGraph、LlamaIndex 和 Google 的 ADK 的框架适配器。核心 Python SDK:

1pip install toolbox-core
1import asyncio2from toolbox_core import ToolboxClient34asyncdefmain():5asyncwith ToolboxClient("http://127.0.0.1:5000") as client:6# Load the toolset and hand the tools to your agent framework…7        tools = await client.load_toolset("semantic_search")89# …or invoke a tool directly. The string is embedded server-side;10# your application never touches a vector.11        search = await client.load_tool("search_docs")12        results = await search(query="how do I make queries faster?")13print(results)1415asyncio.run(main())

或者直接将工具放入 LangGraph 代理:

1from toolbox_langchain import ToolboxClient2from langgraph.prebuilt import create_react_agent34asyncwith ToolboxClient("http://127.0.0.1:5000") as client:5    tools = client.load_toolset("semantic_search")6    agent = create_react_agent(model, tools)

无论哪种方式,嵌入步骤都保留在 Toolbox 服务器端 - 更换gemini-embedding-001为另一个模型或更改维度,应用程序代码无需更改。

生产注意事项

在将其用于演示之外之前,有几件事值得了解:

  • 批处理。如果单次调用有多个使用相同模型的嵌入参数,Toolbox 会将它们批处理为一次嵌入 API 调用。但每次工具调用仍然内联嵌入 - 我的 57 篇文档意味着 57 次到 Vertex AI 的往返,这在 4.1 秒内没问题,但在百万行时非常不可行。对于批量摄取,离线嵌入并使用适当的批处理管道加载(每次INSERT数万行,或async_insert=1如果必须涓流)。将insert_doc保留给代理驱动的一次一条写入。
  • 返回空而不是噪音。正如晚餐查询所示,top-k 总是返回 k 行。为代理将采取行动的搜索工具添加距离阈值。
  • 驱动转义参数,而不是预准备语句。常规参数由clickhouse-go从类型化值序列化和转义,这消除了注入类 - 但语句作为一条插值字符串(包括向量字面量)到达服务器,所以不要将其描述为服务器端绑定,并期待庞大的query_log条目。Toolbox 还支持templateParameters用于表名等,这些是纯字符串替换,完全没有转义,所以将任何用户控制的内容保留在常规参数中。
  • 精确与近似。 cosineDistance没有索引是精确的且 O(n);vector_similarityHNSW 索引使其近似且快速。在 57 行时,索引占部分的 29.8% 且毫无收益,所以在此规模下暴力搜索就是全部。在切换到 ANN 之前,在自己的数据上测试召回率。
  • 无法设置嵌入任务类型。 SEMANTIC_SIMILARITY是硬编码的,因此 Google 推荐的RETRIEVAL_DOCUMENT/RETRIEVAL_QUERY不对称配对不可用。如果检索质量是你优化的重点,请在 Toolbox 外部嵌入。
  • 可观测性和认证。Toolbox 开箱即用地发出 OpenTelemetry 追踪(包括嵌入调用)和指标,工具可以要求经过认证的调用 - 在暴露写入工具(如insert_doc)之前值得接线。
  • 绑定地址和来源。默认监听器仅回环,但--allowed-origins和--allowed-hosts都默认为*,Toolbox 每次启动都会警告 DNS 重绑定风险。请明确设置它们。

总结

这里的模式远远超出了玩具文档表:任何具有Array(Float32)列的 ClickHouse 表都可以通过大约四十行 YAML 变成代理可搜索的 - 只需声明哪些参数表示文本以及哪个模型将它们转换为向量。同样的embeddedBy机制驱动 Postgres 上的 pgvector,这是代码库中唯一具有向量格式化器的其他引擎。

粗糙的边缘主要在管道而非想法 - Gemini 是唯一的嵌入提供商,REST 端点需要选择加入标志,并且获得新环境变量的配置需要重启而不是重新加载。不是管道的是硬编码的SEMANTIC_SIMILARITY任务类型,这给检索质量设了上限。鉴于 ClickHouse 在列式数据上的暴力向量性能和成熟的 HNSW 索引,这种组合 - MCP Toolbox 用于代理边界,ClickHouse 用于存储和排序 - 仍然是我见过的从“我们在表中有文本”到“我们的代理可以语义搜索”的最快路径之一。

MCP Toolbox 是开源的(Apache 2.0),位于github.com/googleapis/mcp-toolbox。完整文档在mcp-toolbox.dev,包括ClickHouse 集成和嵌入模型参考。已针对 Toolbox 1.9.0 和 ClickHouse Cloud 26.4.1 测试。

这篇内容对你有用吗?

反馈只用于改善内容筛选,不等同于收藏

分享这条资讯
分享海报
保存图片
iOS 也可以长按图片保存