298 lines
9.4 KiB
Markdown
298 lines
9.4 KiB
Markdown
# 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 就无法协同工作。
|