Files
SuperBizAgent-java/docs/learning/02-outputKey-深度解析.md
2026-05-31 21:45:14 +08:00

298 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# outputKey 深度解析
> 创建日期:2026-05-30
> 相关文件:`AiOpsService.java`
> 核心概念:Agent 状态共享机制
---
## 🎯 outputKey 是什么?
**一句话总结**:`outputKey` 是 **Agent 状态共享的关键机制**,就像是一个**共享内存的地址**。
---
## 📚 核心机制
```
┌─────────────────────────────────────────────────────────┐
│ OverAllState │
│ (类似一个全局的 Map<String, Object>) │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Key Value │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ "planner_plan" → Planner 的输出 (AssistantMessage)│ │
│ │ "executor_feedback" → Executor 的输出 (JSON) │ │
│ │ "input" → 最初的任务输入 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## 🔄 工作流程(3 步)
### Step 1: Planner 写入
**代码**:
```java
// AiOpsService.java:108
ReactAgent plannerAgent = ReactAgent.builder()
.outputKey("planner_plan") // ← 声明:我要写入 "planner_plan" 这个 key
.build();
// 执行后,Planner 的输出会自动写入到:
// state.put("planner_plan", plannerAgent的输出)
```
---
### Step 2: Executor 读取 & 写入
**Prompt 中引用**:
```java
// AiOpsService.java:147 - Planner 的 Prompt 中
"1. 读取当前输入任务 {input} 以及 Executor 的最近反馈 {executor_feedback}。"
// ^^^^^^^^^^^^^^^^^^^^^
// 这是从 state 中读取的!
```
**关键点**:Prompt 中的 `{executor_feedback}` 会被自动替换为:
```java
state.get("executor_feedback")
```
**Executor 写入**:
```java
// AiOpsService.java:123
ReactAgent executorAgent = ReactAgent.builder()
.outputKey("executor_feedback") // ← Executor 写入这个 key
.build();
```
---
### Step 3: 从 state 中提取最终结果
**代码**:
```java
// AiOpsService.java:83
Optional<AssistantMessage> plannerFinalOutput = state.value("planner_plan")
.filter(AssistantMessage.class::isInstance)
.map(AssistantMessage.class::cast);
String reportText = plannerFinalOutput.get().getText();
```
---
## ⏱️ 完整时间线示例
```
时间线 ───────────────────────────────────────────────────►
1️⃣ Supervisor 启动
state = {}
2️⃣ Supervisor 调用 Planner
Planner: "需要查询告警,decision=EXECUTE"
state = {
"planner_plan": "需要查询告警,decision=EXECUTE"
}
3️⃣ Supervisor 读取 decision=EXECUTE,调用 Executor
Executor 读取: {planner_plan} = "需要查询告警,decision=EXECUTE"
Executor 调用工具: queryPrometheusAlerts()
Executor: "查询成功,发现 3 个告警"
state = {
"planner_plan": "需要查询告警,decision=EXECUTE",
"executor_feedback": "查询成功,发现 3 个告警" ← 新增
}
4️⃣ Supervisor 再次调用 Planner(重新规划)
Planner 读取: {executor_feedback} = "查询成功,发现 3 个告警"
Planner: "需要查询日志,decision=EXECUTE"
state = {
"planner_plan": "需要查询日志,decision=EXECUTE", ← 更新
"executor_feedback": "查询成功,发现 3 个告警"
}
5️⃣ Supervisor 调用 Executor
Executor 读取: {planner_plan} = "需要查询日志,decision=EXECUTE"
Executor 调用工具: queryLogs()
Executor: "查询成功,找到 OOM 日志"
state = {
"planner_plan": "需要查询日志,decision=EXECUTE",
"executor_feedback": "查询成功,找到 OOM 日志" ← 更新
}
6️⃣ Supervisor 再次调用 Planner(最终生成报告)
Planner 读取: {executor_feedback} = "查询成功,找到 OOM 日志"
Planner: "decision=FINISH,输出完整 Markdown 报告"
state = {
"planner_plan": "# 告警分析报告\n...", ← 最终报告
"executor_feedback": "查询成功,找到 OOM 日志"
}
7️⃣ Supervisor 结束,返回 state
8️⃣ Controller 提取报告
finalReport = state.value("planner_plan")
```
---
## 🤔 为什么需要 outputKey?
| 场景 | 没有 outputKey | 有 outputKey |
|------|---------------|--------------|
| **Agent 间通信** | 无法传递数据 | ✅ 通过 state 共享 |
| **重新规划** | Planner 读不到 Executor 的结果 | ✅ 读取 `{executor_feedback}` |
| **最终提取** | 不知道从哪里读取报告 | ✅ `state.value("planner_plan")` |
| **调试** | 无法追踪中间状态 | ✅ 可以打印整个 state |
---
## 💻 等价代码理解
如果你熟悉 JavaScript,可以这样理解:
```javascript
// 没有 outputKey 的版本(行不通)
const plannerOutput = plannerAgent.invoke(input);
const executorOutput = executorAgent.invoke(???); // 😱 怎么传递 plannerOutput?
// 有 outputKey 的版本
const state = {};
plannerAgent.invoke(input, state); // 写入 state["planner_plan"]
executorAgent.invoke(state); // 读取 state["planner_plan"],写入 state["executor_feedback"]
plannerAgent.invoke(state); // 读取 state["executor_feedback"],更新 state["planner_plan"]
```
---
## 📖 Prompt 中的占位符替换
### 原始 Prompt
```java
// AiOpsService.java:147
"1. 读取当前输入任务 {input} 以及 Executor 的最近反馈 {executor_feedback}。"
```
### 替换后的实际 Prompt(发送给 LLM)
```
1. 读取当前输入任务 你是企业级 SRE,接到了自动化告警排查任务... 以及 Executor 的最近反馈 {"status":"SUCCESS","summary":"查询成功,发现3个告警"}。
```
### 替换规则
| 占位符 | 查找位置 | 值来源 |
|--------|---------|--------|
| `{input}` | `state.value("input")` | Supervisor 初始调用时的 taskPrompt |
| `{planner_plan}` | `state.value("planner_plan")` | Planner Agent 的 outputKey |
| `{executor_feedback}` | `state.value("executor_feedback")` | Executor Agent 的 outputKey |
---
## 🎯 核心洞察
**outputKey 的本质**:
1. **写入地址**:Agent 把输出写入 `state[outputKey]`
2. **读取地址**:Prompt 中的 `{outputKey}` 会被替换为 `state[outputKey]`
3. **共享内存**:所有 Agent 共享同一个 `OverAllState` 对象
**类比**:
- `outputKey` 就像文件系统的路径
- `OverAllState` 就像文件系统本身
- Planner 写入 `/planner_plan`
- Executor 读取 `/planner_plan`,写入 `/executor_feedback`
- Supervisor 协调读写顺序
---
## 💡 实践建议
### 1. 命名规范
```java
// ✅ 好的命名(表达角色 + 数据类型)
.outputKey("planner_plan") // Planner 的计划
.outputKey("executor_feedback") // Executor 的反馈
.outputKey("reviewer_verdict") // Reviewer 的判决
// ❌ 差的命名
.outputKey("output") // 太泛,不知道谁的输出
.outputKey("data") // 太泛
.outputKey("result1") // 没有语义
```
### 2. 在 Prompt 中引用
```java
// Executor 的 Prompt
"读取 Planner 最新输出 {planner_plan},只执行其中的第一步。"
// ^^^^^^^^^^^^^^^
// 会被自动替换为 state.get("planner_plan")
// Planner 的 Prompt (Replanner 角色)
"读取 Executor 的最近反馈 {executor_feedback}。"
// ^^^^^^^^^^^^^^^^^^^
// 会被自动替换为 state.get("executor_feedback")
```
### 3. 提取最终结果
```java
// 从 state 中提取
Optional<AssistantMessage> finalOutput = state.value("planner_plan")
.filter(AssistantMessage.class::isInstance)
.map(AssistantMessage.class::cast);
String reportText = finalOutput.get().getText();
```
---
## 🔍 调试技巧
在 `AiOpsService.java:70` 的 `invoke` 调用后打印 state:
```java
Optional<OverAllState> stateOptional = supervisorAgent.invoke(taskPrompt);
// 添加调试代码
if (stateOptional.isPresent()) {
OverAllState state = stateOptional.get();
logger.debug("Final State Keys: {}", state.keys()); // 打印所有 key
logger.debug("Planner Plan: {}", state.value("planner_plan"));
logger.debug("Executor Feedback: {}", state.value("executor_feedback"));
}
```
你会看到类似:
```
Final State Keys: [input, planner_plan, executor_feedback]
Planner Plan: Optional[AssistantMessage{text="# 告警分析报告..."}]
Executor Feedback: Optional[AssistantMessage{text="{"status":"SUCCESS",...}"}]
```
---
## 📚 相关阅读
- [AI Ops 核心设计 - Essence 报告](./01-AI-Ops-核心设计-Essence报告.md)
- [3个核心疑问解答](./03-核心疑问解答.md)
---
> 💡 **总结**:outputKey 是 Agent 间通信的桥梁,没有它,3 个 Agent 就无法协同工作。