从零到一:我用 Java 写了一个 AI Agent,接入 5 个数据源自动排查投放数据
不到 5000 行代码,4 个依赖,实现一个能自主推理、调用工具、跨平台排查数据的 AI Agent。
本文记录完整的建设思路、核心架构。
一、为什么要做这件事
我在公司负责广告投放相关的后端工作。投放数据从头到脚设计很多系统,不知道哪一个地方就出错了:
| 数据源 | 定位 | 查询方式 |
|---|---|---|
| MySQL(DMS) | 在线业务库 | SQL 查询 |
| ClickHouse | 离线分析底表 | Backdoor 接口 |
| Hive | 数仓底表 | Berserker adhoc SQL |
| 代理商平台 | 面向代理商的前端展示 | HTTP API + 模拟登录 |
| 三连平台 | 面向广告主的前端展示 | HTTP API + 模拟登录 |
当用户反馈"数据对不上"时,排查流程极其繁琐:
- 先拿 accountId 反查代理商信息
- 登录代理商平台查数据
- 登录三连平台查数据
- 写 SQL 查 MySQL
- 构造条件查 ClickHouse
- 再查一遍 Hive
- 手动对比 5 组结果,找出差异
这套流程平均耗时 30-60 分钟,而且容易出错。
核心目标:让 AI 自动完成以上全部步骤,一句话输入,全量数据输出。
二、什么是 AI Agent
AI Agent 不是简单的"聊天机器人",它的核心区别在于 自主决策 + 工具调用。
普通 Chat: 用户提问 → AI 回答(纯文本)
AI Agent: 用户提问 → AI 思考 → 调用工具 → 拿到结果 → 继续思考 → 再调工具 → ... → 最终回答
大模型本身不能访问数据库、不能调 HTTP 接口、不能读文件。但通过 Tool Calling(工具调用) 机制,我们可以把这些能力"借"给它。
AI 负责 推理和决策(查什么、怎么查、查完怎么分析),Java 代码负责 执行(发 HTTP、跑 SQL、解析结果)。
这就是 ReAct(Reasoning + Acting) 模式,目前 AI Agent 最主流的架构。
三、整体架构
┌─────────────────────────────────────────────────────────┐
│ Main.java (REPL) │
│ 交互式命令行 / 调试日志输出 │
└──────────────────────────┬──────────────────────────────┘
│ 用户输入
▼
┌─────────────────────────────────────────────────────────┐
│ Agent.java │
│ │
│ ReAct 循环(最多 15 轮): │
│ │
│ ┌─ Claude 推理 ──────────────────────────────────┐ │
│ │ "用户要查展示量,我先调 query_account_info" │ │
│ │ → stop_reason = tool_use │ │
│ └────────────────────────────────────────────────┘ │
│ ↓ 执行工具,结果送回 │
│ ┌─ Claude 推理 ──────────────────────────────────┐ │
│ │ "拿到 bid 了,调 query_launch_data(source=all)" │ │
│ │ → stop_reason = tool_use │ │
│ └────────────────────────────────────────────────┘ │
│ ↓ 5 个数据源结果送回 │
│ ┌─ Claude 推理 ──────────────────────────────────┐ │
│ │ "对比 5 个数据源:MySQL=100K, CK=100K, │ │
│ │ Hive=100K, 代理商=105K, 三连=100K │ │
│ │ → 代理商多了 5K,可能是..." │ │
│ │ → stop_reason = end_turn │ │
│ └────────────────────────────────────────────────┘ │
│ │
└──────────────────────────┬──────────────────────────────┘
│ 工具调用
▼
┌─────────────────────────────────────────────────────────┐
│ ToolRegistry.java │
│ 定义工具 JSON Schema + 分发执行 │
│ │
│ query_launch_data ──→ LaunchDataQueryTool (统一门面) │
│ query_account_info ──→ AccountInfoQueryTool │
│ query_logs ──────────→ LogQueryTool │
│ execute_sql ─────────→ SqlQueryTool │
│ execute_ck_sql ──────→ CkBackdoorQueryTool │
│ execute_hive_sql ────→ HiveQueryTool │
│ ... │
└──────────────────────────┬──────────────────────────────┘
│
┌──────────┬───────┼───────┬──────────┐
▼ ▼ ▼ ▼ ▼
MySQL ClickHouse Hive 代理商平台 三连平台
项目结构:
src/main/java/com/bilibili/ai/
├── Main.java # CLI 入口 (REPL)
├── Agent.java # ReAct 核心循环
├── AnthropicClientFactory.java # Claude SDK 封装
├── config/
│ └── AppConfig.java # 应用配置管理
├── prompts/
│ └── SystemPrompt.java # 系统提示词
└── tools/
├── ToolRegistry.java # 工具注册 + 分发
├── LaunchDataQueryTool.java # 统一查询门面(5源)
├── LaunchQueryUtils.java # 公共工具方法
├── LaunchMetricQueryTool.java # MySQL 查询
├── CkBackdoorQueryTool.java # ClickHouse 查询
├── HiveQueryTool.java # Hive 查询
├── AgentDataQueryTool.java # 代理商平台
├── PlatformDataQueryTool.java # 三连平台
├── AccountInfoQueryTool.java # 账号信息反查
├── SqlQueryTool.java # MySQL DMS 裸执行
├── LogQueryTool.java # 日志查询
└── TimeUtils.java # 时间工具
四、核心知识点详解
4.1 ReAct 循环:Agent 的心脏
ReAct 是整个 Agent 最核心的设计模式。原理非常简单:
// Agent.java 核心逻辑(简化版)
for (int i = 0; i < MAX_ITERATIONS; i++) {
// 1. 把对话历史 + 系统提示词 + 工具定义 发给 Claude
Message response = client.messages().create(params);
// 2. 看 Claude 想干什么
if (response.stopReason == END_TURN) {
return response.text; // AI 认为已经回答完毕
}
if (response.stopReason == TOOL_USE) {
// 3. AI 要调工具 → 执行 → 把结果喂回去
String result = ToolRegistry.executeTool(toolName, inputJson);
conversationHistory.add(toolResult);
continue; // 继续循环
}
}
关键点:
-
循环不是写死的,是 Claude 自己决定要不要继续调工具。当它认为信息足够了,就会返回
end_turn - 对话历史是有状态的,每一轮工具调用的结果都会追加到历史中,Claude 能看到之前所有轮次的信息
- 最大轮次是安全阀(我设了 15 轮),防止 AI 无限循环
4.2 Tool Calling:让 AI 长出"手脚"
Tool Calling 是 Claude API 的原生能力。你需要做两件事:
第一步:告诉 Claude 有哪些工具(JSON Schema)
Tool.builder()
.name("query_launch_data")
.description("统一投放指标查询,覆盖 5 个数据源...")
.inputSchema(Tool.InputSchema.builder()
.properties(Properties.builder()
.putAdditionalProperty("account_id", Map.of(
"type", "integer",
"description", "投放账户 ID"))
.putAdditionalProperty("date", Map.of(
"type", "string",
"description", "查询日期,格式 yyyy-MM-dd"))
.putAdditionalProperty("source", Map.of(
"type", "string",
"description", "数据源: mysql/clickhouse/hive/agent/platform/all",
"default", "all"))
.build())
.addRequired("account_id")
.addRequired("date")
.build())
.build();
Claude 会根据 name、description、参数名和参数描述来理解每个工具的用途,在推理时自主决定调哪个工具、传什么参数。
第二步:当 Claude 返回 tool_use 时,执行对应的 Java 方法
public static String executeTool(String toolName, String inputJson) {
JSONObject input = JSON.parseObject(inputJson);
return switch (toolName) {
case "query_launch_data" -> LaunchDataQueryTool.query(
input.getLongValue("account_id"),
input.getString("date"),
input.getString("metric"),
...);
case "query_account_info" -> AccountInfoQueryTool.queryAccountInfo(
input.getLongValue("account_id"));
default -> errorJson("未知工具: " + toolName);
};
}
整个 ToolRegistry 就是一个 桥梁:一侧是 Claude 的 JSON Schema 定义,另一侧是 Java 方法调用。
4.3 System Prompt:Agent 的"灵魂"
System Prompt 不是随便写两句话。它决定了 AI 的行为模式,我的 System Prompt 包含:
| 模块 | 作用 |
|---|---|
| 角色定义 | "你是B站广告投放数据排查与日志分析助手" |
| 工具总览表 | 所有工具的一句话说明,让 AI 快速知道能调什么 |
| 每个工具的详细说明 | 参数、返回值、使用场景 |
| 排查工作流程 | 关键! 明确告诉 AI 排查步骤的顺序 |
| 字段映射参考 | 两个平台同一指标字段名不同,AI 需要知道对应关系 |
其中最关键的是工作流程。如果不写,AI 可能只查 1-2 个数据源就给结论。写了之后:
### 投放数据排查工作流程
1. `query_account_info` — 如果缺少 bid/agentAccountId,先反查
2. `query_launch_data`(source=all,传入 bid + agent_account_id)— 一次查全部 5 个数据源
查完后汇总对比 5 个数据源的值,明确标注哪些一致、哪些不一致,定位差异环节。
这段话把 AI 的行为"锚定"了:它会先确认参数是否齐全,然后一次性查 5 个源,最后结构化对比。
4.4 统一查询门面(Facade 模式)
这是经过 3 轮重构后的最终形态。最初,5 个数据源是 5 个独立工具,AI 需要分 5 次调用。问题是:
- Token 浪费:每次工具调用都需要完整的对话历史上下文
- 调用次数多:5 次调用 = 5 轮 ReAct 循环 = 5 次 API 请求
- 容易遗漏:AI 有时会"偷懒"只查 3 个
重构后,一个 query_launch_data(source=all) 搞定一切:
public static String query(long accountId, String date, String metric,
String dimension, String source,
long bid, long agentAccountId) {
return switch (source) {
case "mysql" -> queryMysql(accountId, date, metric, dimension);
case "clickhouse" -> queryCk(accountId, date, metric, dimension);
case "hive" -> queryHive(accountId, date, metric, dimension);
case "agent" -> queryAgent(bid, accountId, agentAccountId, date, dimension);
case "platform" -> queryPlatform(accountId, date, dimension);
case "all" -> queryAll(...); // 依次查 5 个,合并结果
default -> errorJson("不支持的数据源: " + source);
};
}
queryAll() 内部依次查 5 个数据源,将结果合并成一个大 JSON 返回。对 AI 来说是 1 次工具调用,但背后跑了 5 个完全不同的查询逻辑。
4.5 五个数据源的接入方式
每个数据源的接入方式完全不同,这是这个项目最复杂的部分。
MySQL(DMS 平台)
客户端 → DMS check_sql API(校验 SQL)→ DMS query_data API(执行)→ 分页结果
ClickHouse
客户端 → ClickHouse check_sql API(校验 SQL)→ DMS query_data API(执行)→ 分页结果
Hive(Berserker 异步执行)
这是最复杂的一个,三步异步流程:
1. POST /api/adhoc/sql/run/execute → 拿到 queryId
2. WebSocket 连接 /api/adhoc/socket/{queryId} → 触发真正的执行(连上就断开)
3. HTTP 轮询 /api/adhoc/sql/run/result?queryId=xxx → 每 3 秒轮询,最多 90 秒
代理商平台 & 三连平台(模拟登录 + HTTP API)
两个平台套路类似:
1. 模拟登录 → 拿到 Token
2. 查询订阅字段(知道平台展示了哪些指标)
3. 查询投放数据(自动翻页,page_size=100)
4. 过滤零值字段(200+ 字段 → 只保留有值的)
4.6 Token 优化:省钱的关键
大模型 API 按 Token 计费。Token 可以简单理解为"文本片段":
- 1 个英文单词 ≈ 1.3 tokens
- 1 个中文汉字 ≈ 1.5 tokens
- Claude Opus 输入
75/百万 token
平台返回的数据动辄 200+ 字段,一次查询原始数据 ~285KB。如果不优化,一轮对话光工具结果就要花费好几块钱。
优化手段 1:过滤零值字段
// 优化前:{"show_count": 1000, "click_rate": 0, "ctr": 0.0, "field_3": "", ...} // 200+ 字段
// 优化后:{"show_count": 1000} // 只保留有值的
public static JSONObject filterZeroFields(JSONObject resultObj) {
// 遍历每一行,移除值为 0、0.0、""、null 的字段
}
效果:285KB → 28KB,减少 90%。
优化手段 2:Token 统计监控
每一轮 API 调用都记录 input/output token 数和各消息大小:
[第 2 轮] Token 统计: input=15234, output=892, total=16126
消息大小明细: system=2.1KB, msg[0](USER)=0.1KB, msg[1](ASSISTANT)=0.5KB, msg[2](USER)=27.3KB
这让我能精确定位哪条消息在"吃" Token,针对性优化。
4.7 字段知识库:AI 的"字典"
launch_document.json 是整个系统的知识库(~250KB),定义了 200+ 个投放指标字段:
{
"fields": [
{
"key": "show_count",
"description": "展示量",
"type": "raw",
"database": "business_charging_02",
"queries": {
"account_day": "SELECT SUM(show_count) FROM 表名 WHERE ..."
}
},
{
"key": "click_through_rate",
"description": "点击率",
"type": "computed",
"formula": "click_count / show_count * 100",
"hint": "需要先查 click_count 和 show_count"
}
]
}
AI 查询时输入"展示量"或 show_count 都能匹配到正确字段。computed 类型的字段会提示 AI 先查依赖字段再计算。
4.8 Anthropic Java SDK 的使用
项目基于官方 anthropic-java SDK(v2.14.0),核心用法:
// 1. 创建客户端
AnthropicClient client = AnthropicOkHttpClient.builder()
.baseUrl("https://api.anthropic.com")
.apiKey("sk-xxx")
.timeout(Duration.ofSeconds(120)) // 大模型推理慢,超时要长
.maxRetries(2) // 自动重试
.build();
// 2. 构建请求
MessageCreateParams params = MessageCreateParams.builder()
.model("claude-opus-4-6-20250205")
.maxTokens(4096)
.system(systemPrompt) // 系统提示词
.messages(conversationHistory) // 完整对话历史
.addTool(tool1) // 工具定义
.addTool(tool2)
.build();
// 3. 发送请求
Message response = client.messages().create(params);
// 4. 处理响应
for (ContentBlock block : response.content()) {
if (block.isText()) { /* AI 的文本回复 */ }
if (block.isToolUse()) { /* AI 要调用工具 */ }
}
SDK 最大的好处是类型安全——工具调用的参数、响应的结构都有明确的 Java 类型,IDE 自动补全非常友好。
4.9 调试与可观测性
每次问答都会生成独立的调试文件:
src/main/resources/agent_result/
├── 20260306_11_19_13_agent_result_debug.txt
├── 20260306_11_52_26_agent_result_debug.txt
└── ...
文件内容包含完整的执行轨迹:
>>> 用户反馈 accountId=123 展示量不对
[第 1 轮] 发送请求到 Claude...
[第 1 轮] Token 统计: input=3421, output=156, total=3577
[Claude 思考] 用户只提供了 accountId,我需要先查询代理商信息
[工具调用] query_account_info({"account_id":123})
[工具结果] {"bid":123, "agentAccountId":456, ...}
[第 2 轮] 发送请求到 Claude...
[工具调用] query_launch_data({"account_id":123, "source":"all", ...})
[工具结果] {"mysql":{...}, "clickhouse":{...}, "hive":{...}, "agent":{...}, "platform":{...}}
[第 3 轮] 发送请求到 Claude...
[最终回答] 对比 5 个数据源的展示量:MySQL=622023, CK=622023, Hive=622023, 代理商=622023, 三连=622023。5 个数据源完全一致,数据无差异。
(耗时 45.2 秒)
这个轨迹对排查 Agent 行为异常非常有价值——能看到 AI 每一步在想什么、调了什么工具、拿到了什么结果。
五、重构演进过程
这个项目不是一步到位的,经历了 3 轮大的重构:
第一版:每个数据源一个工具(7 个投放工具)
query_launch_metric (MySQL)
query_ck_launch_data (ClickHouse)
query_hive_launch_data (Hive)
investigate_agent_data (代理商)
investigate_platform_data (三连)
execute_sql / execute_ck_sql / execute_hive_sql (裸执行)
问题:AI 要调 5-7 次工具,经常遗漏数据源,Token 消耗巨大。
第二版:统一门面(3 个投放工具)← 当前版本
query_launch_data (全部 5 源,source=all) ← 再次合并
query_account_info (前置查询)
execute_sql / execute_ck_sql / execute_hive_sql (裸执行,兜底)
效果:排查全流程从 4-5 次工具调用减为 2 次(先查账号信息,再一次查 5 源)。
核心经验:AI Agent 的工具设计,粒度越粗越好。不要给 AI 太多低层工具让它自己组合,而是封装高层的"一键操作"工具。AI 擅长推理和决策,不擅长复杂的多步骤编排。
六、依赖极简主义
整个项目只有 4 个外部依赖:
<dependencies>
<!-- Claude API SDK -->
<dependency>
<groupId>com.anthropic</groupId>
<artifactId>anthropic-java</artifactId>
<version>2.14.0</version>
</dependency>
<!-- JSON 处理 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.53</version>
</dependency>
<!-- HTTP 客户端 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>5.8.32</version>
</dependency>
<!-- .env 配置加载 -->
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>3.0.2</version>
</dependency>
</dependencies>
没有 Spring Boot,没有 MyBatis,没有 Redis。一个 AI Agent 不需要那些重量级框架。保持简单,才能快速迭代。
七、实际效果
输入
>>> 用户反馈 accountId=123 在2026年3月1号 展示量指标 多平台不对,帮我检查一下
AI 自动执行
[第 1 轮] 调用 query_account_info → 拿到 bid=123, agentAccountId=456
[第 2 轮] 调用 query_launch_data(source=all) → 同时查 5 个数据源
├── [1/5] MySQL: SUM(show_count) = 622023
├── [2/5] ClickHouse: SUM(show_count) = 622023
├── [3/5] Hive: SUM(show_count) = 622023
├── [4/5] 代理商平台: show_count = 622023
└── [5/5] 三连平台: show_count = 622023
[第 3 轮] AI 分析对比,输出结论
输出
## 展示量对比结果
| 数据源 | 展示量 | 状态 |
|--------|--------|------|
| MySQL | 622,023 | ✅ |
| ClickHouse | 622,023 | ✅ |
| Hive | 622,023 | ✅ |
| 代理商平台 | 622,023 | ✅ |
| 三连平台 | 622,023 | ✅ |
**结论**:5 个数据源完全一致,展示量无差异。
耗时:约 45 秒(主要花在 Hive 异步查询和平台模拟登录上)
人工排查同样内容:30-60 分钟
八、总结
构建一个生产可用的 AI Agent,核心知识点:
| 知识点 | 本项目的实践 |
|---|---|
| ReAct 模式 | Agent.java 的核心循环 |
| Tool Calling | ToolRegistry 定义 + 分发 |
| Prompt Engineering | SystemPrompt 中的工具说明和工作流程 |
| Facade 设计模式 | LaunchDataQueryTool 统一 5 个数据源 |
| Token 优化 | filterZeroFields 过滤零值、字段压缩 |
| 异步轮询 | Hive 查询的 WebSocket 触发 + HTTP 轮询 |
| 模拟登录 | 代理商/三连平台的 Cookie + Token 认证 |
| 知识库设计 | launch_document.json 字段映射 |
| 可观测性 | Token 统计、调试日志、执行轨迹 |
最终,不到 5000 行 Java 代码、4 个依赖,实现了一个能跨 5 个异构数据源自主排查问题的 AI Agent。
核心经验:AI Agent 的价值不在于 AI 本身有多智能,而在于你能给它接入多少"工具"。大模型是大脑,工具是手脚。手脚越多、越灵活,Agent 就越强大。
本项目技术栈:Java 17 + Anthropic Claude API + FastJSON2 + Hutool HTTP