多智能体协作与ClickHouse事件存储:构建AI应用护城河的工程实践

📅 发布时间:2026/9/3 16:00:06
多智能体协作与ClickHouse事件存储:构建AI应用护城河的工程实践
模型能力每个月都在刷新但真正决定 AI 应用能走多远的往往是工程侧的问题多个 Agent 之间怎么协作私有数据和评估资产放在哪里以及当会话量和日志量变大之后用什么基础设施把这些数据沉淀下来。这次 BestBlogs 早报里被反复讨论的 OpenClaw 2.0 协作、AI 应用护城河以及 ClickHouse 智能体基础设施恰好把这个问题链串了起来。这篇文章不是简单复述早报内容而是希望把三个关键词拆成一条可落地的工程主线。后面会先解释多智能体协作为什么不能只靠“让模型多聊几轮”再聊护城河的真实组成随后重点进入 ClickHouse 作为智能体事件存储的实战部分包括 Docker 部署、事件表设计、Java Web 项目接入以及高频率出现的认证失败问题排查。如果你是正在做 Agent 应用、智能体平台或者准备把会话日志、工具调用记录、成本数据统一收口的数据开发这篇文章会比较适合当作一份可以直接抄作业的参考。1. 为什么这三个关键词需要放在一起看1.1 OpenClaw 2.0、护城河、ClickHouse 是什么关系初看之下OpenClaw 2.0 是一个智能体工具层面的词AI 应用护城河是产品战略层面的词ClickHouse 则是数据基础设施层面的词。三者看起来不太相关但放到实际工程里是一条完整链路。过去的 AI 应用多数是“用户输入一句话模型返回一段文字”。这种场景下应用层只需要关心 Prompt、上下文长度和模型输出质量基础设施压力并不大。但到了多智能体阶段事情开始变得复杂。一个用户请求可能会被拆成多个子任务分别交给规划 Agent、工具调用 Agent、审校 Agent 去处理。子任务之间需要共享业务上下文每个 Agent 都可能调用不同工具而且外部工具不一定允许无条件执行。与此同时整个多智能体执行过程会产生大量结构化事件谁调用了哪个工具、调用耗时多久、模型输出了多少 token、成功还是失败。这就是 OpenClaw 2.0 协作、AI 应用护城河、ClickHouse 智能体基础设施三者之间的真实关系多智能体协作需要工具链和工作流沉淀否则每次任务都是“一次性代码”应用想在模型同质化时代守住用户必须保留自己的交互数据、评估数据和业务工作流而这些数据落到 ClickHouse 这类分析型数据库里才能支撑后续的会话回放、效果评估、成本核算和异常追溯。换句话说护城河不是模型带来的而是工作流和数据资产共同沉淀出来的。1.2 本文适合哪些读者如果你属于下面任何一类本文内容都会有比较直接的帮助正在搭建多智能体应用但不确定 Agent 之间的协作模式该怎样设计团队已经在使用 Dify、OpenClaw 或其他智能体框架希望把日志和会话数据统一管理使用 ClickHouse 作为业务或日志分析库遇到过连接失败、认证失败、数据模型设计不合理的问题需要从 Java Web 项目中连接 ClickHouse但不希望继续使用古老驱动踩版本坑。2. 多智能体协作的工程化方向2.1 多 Agent 协作先要解决的是任务边界很多刚接触多智能体的人会以为只要把多个大模型 API 放在同一个系统里让它们互相传递消息就是“多智能体”。这种理解并不准确。从工程角度看多智能体协作更接近一个“有边界的分布式系统”。系统里每个 Agent 都有自己的职责有的负责拆解目标有的负责调用搜索、数据库、IM 等外部工具有的负责检查最终输出质量。它们之间需要一份明确的协议来约定任务由谁发起中间产物存放在什么位置一个 Agent 执行失败后由哪个角色接管或重试工具调用是否需要人工审批一次完整任务的执行链路如何做日志还原。如果这些边界不清晰多 Agent 的最终效果往往是“看似热闹实则不可维护”。把边界梳理清楚之后再去看 OpenClaw 2.0 这类工具就会更容易理解社区里为什么把协作能力当成重点。因为工具只是在帮你落地这套边界不能替代你决策。2.2 Workspace、Skill 与权限审批在社区讨论和实际部署中经常能看到几个和 OpenClaw 相关的名词workspace、skill、exec-approvals.json。这几个词看起来是具体项目产物但背后代表的是多智能体工程里最通用的三类问题。第一类是工作区。Agent 执行任务时需要读取输入、写中间结果、保存最终产物。工作区就是给每个任务或者每个 Agent 提供的持久化目录。划分工作区的意义在于隔离避免多个 Agent 同时读写同一批文件造成结果互相覆盖。第二类是技能包。技能包可以理解成“把某类外部能力封装成 Agent 能理解、能调用的接口”。例如封装一个 Git 操作、一个数据库查询、一个内部审批接口都属于技能包范畴。技能包越多Agent 能完成的真实业务动作就越多但同时需要越严格的权限校验。第三类是权限审批。一些旧版配置中会出现 exec-approvals.json 之类的文件里面记录了哪些命令可以自动执行、哪些需要人工审批。当你从老版本迁移时如果看到类似 approval 配置文件仍然存在不要盲目直接删除。正确做法是确认旧审批规则是否覆盖了当前工作流然后一次性迁移或收敛配置避免出现“旧规则放行危险命令新规则又完全禁用了必要操作”的问题。2.3 模型接入与可观测性要分开看待多智能体协作过程中模型接入是相对容易变化的部分。早期你可能接入某个大模型 API后来因为成本或效果原因切换到另一个。此时如果应用层直接把模型名硬编码在业务逻辑里替换成本会非常高。更好的做法是把模型接入看作一个可替换的配置层通过统一的 provider endpoint 和 model id 来对接不同模型服务。社区中一些部署问题例如 Agent 启动后提示 unknown model多数不是模型不存在而是配置的模型 ID 与供应商服务端不一致。排查时优先检查配置文件中的 model 名称、endpoint 地址、API Key 是否有对应权限。可观测性则是另一件事。无论底层使用哪家模型每一次调用的 session、tool、token 消耗、延迟、错误码都应该被完整记录下来。只有记录足够完整才可能在模型切换之后通过历史数据分析新版模型是否真的更好。3. AI 应用的护城河到底在哪里3.1 模型能力本身很难构成护城河如果一款 AI 应用的核心竞争力只是“我接入了某一个大模型”那从长期看几乎不存在壁垒。原因很直接你能调用的模型竞争对手也能调用你使用的 Prompt 技巧也很快会被公开或被优化。底层模型能力迭代很快应用层无法控制也不能阻止别人使用相同能力。所以真正需要关注的是那些“离开你的应用之后就无法带走的资产”。这些资产通常有几个来源业务长期运行沉淀下来的私有数据不断迭代的任务工作流和 Agent 编排逻辑团队积累的数据集、标注集、回归评测集与已有业务系统的身份、权限、审批链路集成用户在使用过程中产生的行为和反馈数据。这些东西组合起来才构成了比较难被复制的应用护城河。3.2 四条值得优先建设的护城河结合目前智能体应用的落地情况有四条护城河值得优先投入。第一条是私有数据。模型没有学习过你的内部业务规则、历史工单、客户沟通记录但你的应用可以借助检索增强生成或者微调把这些数据变成模型输出时的上下文。数据越难被外部获取竞争力越强。第二条是可复用工作流。把“一个销售线索如何被跟进”“一个技术工单如何被分类并分派”这类固定流程制作成 Agent 编排本质上是在把团队经验软件化。工作流一旦沉淀就不会随个别人员的离开而消失。第三条是系统集成链路的深度。如果智能体能直接调用内部 API、读取企业权限体系、执行有审批记录的操作那它就不是一个孤立的对话机器人而是企业系统中的“执行者”。集成越深替换成本越高。第四条是评测与回归体系。没有评测体系的 AI 应用上线之后很难回答“这次 Prompt 改动到底变好了还是变差了”。评测集虽然建设成本高但它是模型迭代和 Agent 工作流迭代的标尺也是护城河的重要组成部分。3.3 护城河和可观测数据是同一件事你可能会发现上面讲的四类护城河里至少有三类都依赖“记录和沉淀”。可复用工作流要有版本记录系统集成要有调用日志评测体系要依赖真实线上会话做样本回收。如果没有一套低成本、高吞吐的数据基础设施去保存这些数据护城河就很难真正形成。ClickHouse 之所以开始在智能体基础设施讨论中频繁出现正是因为这类场景对数据系统的要求不完全是传统在线事务数据库擅长的那种。下面是更具体的对比和落地方式。4. ClickHouse 为什么适合做智能体基础设施4.1 先理解 ClickHouse 的定位ClickHouse 是一个开源的列式数据库定位是联机分析处理。它和 MySQL、PostgreSQL 这类关系型数据库有一个核心区别行式数据库擅长高频更新、事务和单行点查列式数据库则擅长批量写入、大范围扫描和聚合统计。智能体事件数据通常具备两个特征。第一写入量大。每一次用户的提问、模型的回复、工具调用、模型切换、错误重试都可能是一条事件。一个中等规模的智能体应用一天的会话事件量可能轻松达到百万级甚至更高。第二聚合查询多。我们在分析智能体效果时很少关心“某一条事件的内容”更多是关心“某个 Agent 昨天平均响应耗时多少”“哪个 SKill 调用失败率最高”“这周活跃会话数是多少”。这两个特征正好落在 ClickHouse 的舒适区内。相比之下把事件数据都打成数据库表存进 MySQL前期也许能跑但数据量上来后聚合查询会非常吃力索引也难以覆盖各种条件组合。4.2 ClickHouse 不是用来取代会话状态库的这里需要做一个边界说明ClickHouse 适合存储历史事件和分析数据但通常不建议把它当作智能体运行时的“会话状态主存储”。多智能体在执行过程中偶尔需要快速读取某个 session 当前进度。这种读写频率高、要求低延迟的状态访问交给 Redis、MySQL 或对象存储会更合适。正确做法是双写运行时状态放在高性能存储中当事件产生后异步把结构化事件写入 ClickHouse用于后续分析和回放。这样能避免 ClickHouse 被高频点查压垮也能让实时链路和分析链路互不干扰。4.3 智能体场景有哪些基础表基于经验智能体基础设施通常需要保留几类数据。第一类是会话表记录 session 的创建时间、创建者、所属应用、结束时间。这一层适合放在 MySQL 或 PostgreSQL 中做精确查询。第二类是事件明细表这是 ClickHouse 里最重要的一类表。每条事件记录 Agent 名称、事件类型、会话 ID、模型名称、token 数、执行时间、结果状态等信息。后续的会话回放、耗时统计、成本统计都依赖这张表。第三类是调用成本表与事件明细类似但更关注 token 数量和费用拆分。第四类是评测结果表用于记录人工标注或模型自动评估的结果帮助后续做回归对比。下面实战部分会以“事件明细表”作为主表从建库到 Java 接入完整走一遍。5. 环境准备5.1 本文演示环境后面的命令和代码建议在下面的环境组合中运行操作系统Linux 或 macOSWindows 用户可以把 Docker 命令放到 WSL2 或 Docker Desktop 中执行容器环境Docker用于快速启动 ClickHouseClickHouse 版本以官方镜像 23.8 系列为例说明生产环境请固定为经过验证的版本Java 版本JDK 8 或更高版本构建工具Maven 或 Gradle智能体框架本文不绑定具体工具你使用 OpenClaw、Dify 或自研 Agent 框架均可。版本需要根据你的项目实际情况调整本文重点演示数据模型和接入思路。5.2 用 Docker 启动 ClickHouse先拉取并启动一个 ClickHouse 容器。为了方便后续 Java 连接这里把 HTTP 端口 8123 和 Native 端口 9000 都映射到宿主机。docker run -d \ --name clickhouse-agent \ --ulimit nofile262144:262144 \ -p 8123:8123 \ -p 9000:9000 \ -e CLICKHOUSE_USERdefault \ -e CLICKHOUSE_PASSWORDclickhouse_123 \ -e CLICKHOUSE_DBagent_analytics \ -v clickhouse_agent_data:/var/lib/clickhouse \ clickhouse/clickhouse-server:23.8命令说明--ulimit nofile262144:262144是 ClickHouse 官方推荐的句柄限制避免运行过程中文件句柄不够用-e CLICKHOUSE_USER和-e CLICKHOUSE_PASSWORD用来设置默认用户 default 的密码-e CLICKHOUSE_DBagent_analytics会让容器启动时自动创建数据库-v clickhouse_agent_data是数据卷存放 ClickHouse 数据文件避免容器删除后数据丢失。注意如果你使用了-v挂载旧数据目录那么新设置的环境变量可能不会立刻生效。因为 ClickHouse 第一次初始化时读的是环境变量之后启动会继续使用已有数据目录中的users.xml。这也是很多“改了密码却依然认证失败”的根因。5.3 验证 ClickHouse 是否启动成功容器启动后进入容器执行查询docker exec -it clickhouse-agent \ clickhouse-client \ --user default \ --password clickhouse_123 \ --query SELECT version()如果一切正常会输出类似下面的内容23.8.4.69实际版本号会随镜像具体 tag 不同而变化。如果你执行到这里就开始报认证失败建议先不要继续往下看直接跳到第 7.4 节处理认证问题否则后面的建表语句都会跑不通。6. 实战为智能体构建 ClickHouse 事件存储6.1 创建表结构下面创建一张智能体事件明细表。这张表会记录一次 Agent 执行过程中的各种事件包括用户消息、模型回复、工具调用、错误等。先进入 clickhouse-clientdocker exec -it clickhouse-agent clickhouse-client \ --user default \ --password clickhouse_123然后执行建表 SQLCREATE DATABASE IF NOT EXISTS agent_analytics; CREATE TABLE IF NOT EXISTS agent_analytics.agent_events ( event_id UUID DEFAULT generateUUIDv4(), agent_name LowCardinality(String), session_id String, event_type LowCardinality(String), -- 事件类型user_message / agent_message / tool_call / tool_result / error model_name LowCardinality(String), llm_tokens UInt32, duration_ms UInt64, success Nullable(Bool), detail String, event_time DateTime64(3, UTC) ) ENGINE MergeTree() PARTITION BY toYYYYMMDD(event_time) ORDER BY (session_id, event_time) TTL event_time INTERVAL 90 DAY;对关键字段说明一下agent_name使用LowCardinality(String)因为 Agent 名称通常只有少量枚举值这种类型能显著提升压缩率和查询效率event_type也使用LowCardinality它在业务上是一组固定值success使用Nullable(Bool)因为有些事件没有“成功/失败”概念例如普通文本记录event_time使用DateTime64(3, UTC)保留毫秒精度并在比较时统一按 UTC 排序避免不同环境时区不一致导致统计错乱TTL event_time INTERVAL 90 DAY表示数据默认保留 90 天过期后 ClickHouse 会自动删除。其中ORDER BY (session_id, event_time)是为了让“按会话回放执行轨迹”的查询更快。如果你的查询更多按 Agent 维度聚合可以调整为主键顺序。6.2 写入测试数据建表完成后插入几条事件模拟一个多智能体会话中产生的日志。INSERT INTO agent_analytics.agent_events (agent_name, session_id, event_type, model_name, llm_tokens, duration_ms, success, detail, event_time) VALUES (planner, s1001, user_message, gpt-4o, 150, 220, 1, user request: help me analyze sales data, now64()), (planner, s1001, agent_message, gpt-4o, 820, 1400, 1, plan created with 3 steps, now64()), (executor, s1001, tool_call, gpt-4o, 300, 850, 1, call clickhouse: query daily orders, now64()), (executor, s1001, tool_result, NULL, 0, 120, 1, query ok, rows: 450, now64()), (reviewer, s1001, agent_message, gpt-4o, 720, 1300, 0, review failed: output missing region filter, now64());这段 SQL 主要模拟的是三个不同 Agent 在同一会话中的事件链路。插入完成后可以执行简单查询验证数据SELECT session_id, agent_name, event_type, duration_ms, success FROM agent_analytics.agent_events WHERE session_id s1001 ORDER BY event_time;结果应该按照事件发生顺序展示出 planner、executor、reviewer 的执行过程。6.3 按小时统计智能体执行情况真实场景中事件表的数据量会非常大。我们通常关心的是趋势和异常例如每个 Agent 每小时处理了多少个请求、工具调用失败率是多少。执行以下查询SELECT toStartOfHour(event_time) AS hour, agent_name, uniqExact(session_id) AS active_sessions, countIf(event_type tool_call) AS tool_calls, countIf(event_type error) AS errors, avgIf(duration_ms, event_type tool_call AND success 1) AS avg_tool_ms FROM agent_analytics.agent_events WHERE event_time now() - INTERVAL 7 DAY GROUP BY hour, agent_name ORDER BY hour, agent_name LIMIT 100;这条 SQL 利用了 ClickHouse 多个聚合函数uniqExact用于精确统计去重会话数countIf用于按条件计数avgIf用于求满足条件的平均耗时。当数据量很大时这种聚合方式明显优于把数据全部拉到应用层再统计。如果希望进一步优化按小时聚合查询性能还可以为这张表增加物化视图让 ClickHouse 在数据插入时就预计算每个小时的结果。简单示例CREATE MATERIALIZED VIEW IF NOT EXISTS agent_analytics.v_agent_hourly ENGINE SummingMergeTree() ORDER BY (agent_name, hour) AS SELECT agent_name, toStartOfHour(event_time) AS hour, countIf(event_type tool_call) AS tool_calls, countIf(event_type error) AS errors, countIf(event_type user_message) AS user_messages FROM agent_analytics.agent_events GROUP BY agent_name, hour;这里需要注意物化视图的结果也是异步生成的插入后不会立刻在视图里出现需要等待几秒钟。如果业务要求实时统计直接在明细表上跑聚合即可。6.4 如何将智能体事件接入 ClickHouse智能体应用接入 ClickHouse 的方式通常有两种一种是采集服务写入。在 Agent 执行链路中增加一个事件旁路只要 Agent 产生了结构化事件就把它发送到消息队列再由一个写入服务批量写入 ClickHouse。这种方式适合高吞吐、事件量大的生产环境。另一种是应用内同步写入。适合开发期或事件量较小的情况直接在 Agent 服务里调用 ClickHouse JDBC 写入。下一节会演示这种方法。7. 实战Java Web 项目连接 ClickHouse7.1 驱动包选择建议Java 项目连接 ClickHouse最常用的是官方 JDBC 驱动。较新的 Artifact 坐标是dependency groupIdcom.clickhouse/groupId artifactIdclickhouse-jdbc/artifactId /dependency版本需要到 Maven Central 或项目仓库中查询选择稳定版本。不建议继续使用很老的ru.yandex.clickhouse包名驱动因为老驱动在协议支持、连接参数、Java 版本兼容性上都已经跟不上新版本服务端。如果你是在维护历史项目尽量规划迁移而不是继续在旧驱动上打补丁。如果项目里还在使用类似 clickhouse 0.9.8 这种非常老的驱动你会发现即使能连上也会遇到一些参数格式不一致、数据类型映射异常的问题。老版本使用的类名通常是ru.yandex.clickhouse.ClickHouseDriver新版官方驱动类名是com.clickhouse.jdbc.ClickHouseDriver。7.2 DriverManager 连接示例下面是一个最小可运行的 Java 示例。它不依赖 Spring只要能正常引入 JDBC 驱动就可以运行。package com.example.clickhouse; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.Properties; public class ClickHouseAgentEventWriter { public static void main(String[] args) { // 新版官方驱动的 URL 一般形如jdbc:clickhouse://host:8123/db String url jdbc:clickhouse://127.0.0.1:8123/agent_analytics; Properties props new Properties(); props.setProperty(user, default); props.setProperty(password, clickhouse_123); props.setProperty(connect_timeout, 10000); props.setProperty(socket_timeout, 600000); String insertSql INSERT INTO agent_analytics.agent_events (agent_name, session_id, event_type, model_name, llm_tokens, duration_ms, success, detail, event_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, now64()) ; try (Connection conn DriverManager.getConnection(url, props); PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setString(1, executor); ps.setString(2, s2001); ps.setString(3, tool_call); ps.setString(4, gpt-4o); ps.setInt(5, 420); ps.setLong(6, 760L); ps.setBoolean(7, true); ps.setString(8, query clickhouse agent_events table); int rows ps.executeUpdate(); System.out.println(inserted rows rows); try (Statement st conn.createStatement(); ResultSet rs st.executeQuery(SELECT count() FROM agent_analytics.agent_events)) { if (rs.next()) { System.out.println(total events rs.getLong(1)); } } } catch (Exception e) { e.printStackTrace(); } } }这段代码说明两点写入 ClickHouse 不一定需要通过 MyBatis、JPA 这类 ORM。事件写入场景直接用 JDBC 批量插入更轻量PreparedStatement 能避免 SQL 拼接注入问题也提升了批次写入时的执行效率。如果你用的仍然是旧驱动URL 可能需要写成jdbc:clickhouse://127.0.0.1:8123/agent_analytics类名则改为ru.yandex.clickhouse.ClickHouseDriver。但整体思路一致。7.3 Spring Boot 项目中如何配置在 Spring Boot 中如果只是给智能体模块增加点击事件分析能力不建议把 ClickHouse 配置