311 lines
8.4 KiB
Markdown
311 lines
8.4 KiB
Markdown
# 3个核心疑问解答
|
||
|
||
> 创建日期:2026-05-30
|
||
> 主题:Prompt 占位符、outputKey 冲突、多 key 读取
|
||
> 相关文件:`AiOpsService.java`
|
||
|
||
---
|
||
|
||
## ❓ 疑问 1:Prompt 中的 `{}` 占位符如何替换?
|
||
|
||
### 机制
|
||
|
||
Spring AI Agent Framework 的**模板引擎自动替换**
|
||
|
||
### 示例
|
||
|
||
**原始 Prompt**:
|
||
```java
|
||
// AiOpsService.java:147
|
||
"读取当前输入任务 {input} 以及 Executor 的最近反馈 {executor_feedback}。"
|
||
```
|
||
|
||
**执行时的替换过程**:
|
||
```
|
||
1. Agent Framework 扫描 Prompt 中的 {} 占位符
|
||
2. 从 OverAllState 中查找对应的 key
|
||
3. 替换为实际值
|
||
```
|
||
|
||
**实际发送给 LLM 的 Prompt**:
|
||
```
|
||
读取当前输入任务 你是企业级 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 |
|
||
|
||
---
|
||
|
||
### 等价代码(简化版)
|
||
|
||
```java
|
||
// 如果你想看替换后的实际 Prompt,可以在 Agent 执行前打印:
|
||
ReactAgent plannerAgent = buildPlannerAgent(chatModel, toolCallbacks);
|
||
|
||
// 内部会做类似这样的事情(简化版):
|
||
String prompt = buildPlannerPrompt(); // 含 {executor_feedback}
|
||
String actualPrompt = prompt.replace(
|
||
"{executor_feedback}",
|
||
state.get("executor_feedback").toString()
|
||
);
|
||
// 然后发送给 LLM
|
||
```
|
||
|
||
---
|
||
|
||
### 代码证据
|
||
|
||
**Planner Prompt 中引用 2 个 key**:
|
||
```java
|
||
// AiOpsService.java:147
|
||
"1. 读取当前输入任务 {input} 以及 Executor 的最近反馈 {executor_feedback}。"
|
||
// ^^^^^^^ ^^^^^^^^^^^^^^^^^^^
|
||
// 第1个key 第2个key
|
||
```
|
||
|
||
**Executor Prompt 中引用 1 个 key**:
|
||
```java
|
||
// AiOpsService.java:243
|
||
"你是 Executor Agent,负责读取 Planner 最新输出 {planner_plan},只执行其中的第一步。"
|
||
// ^^^^^^^^^^^^^^^
|
||
// 从 state 读取
|
||
```
|
||
|
||
---
|
||
|
||
## ❓ 疑问 2:如果两个 Agent 用同一个 outputKey 会怎样?
|
||
|
||
### 后果
|
||
|
||
**后执行的 Agent 会覆盖先执行的 Agent 的输出** ⚠️
|
||
|
||
---
|
||
|
||
### 错误示例
|
||
|
||
```java
|
||
// ❌ 错误示例
|
||
ReactAgent agent1 = ReactAgent.builder()
|
||
.name("agent1")
|
||
.outputKey("shared_key") // ← 相同的 key
|
||
.build();
|
||
|
||
ReactAgent agent2 = ReactAgent.builder()
|
||
.name("agent2")
|
||
.outputKey("shared_key") // ← 相同的 key
|
||
.build();
|
||
|
||
// 执行顺序:
|
||
// 1. agent1.invoke() → state["shared_key"] = "agent1的输出"
|
||
// 2. agent2.invoke() → state["shared_key"] = "agent2的输出" (覆盖!)
|
||
//
|
||
// 最终结果:agent1 的输出丢失了!
|
||
```
|
||
|
||
---
|
||
|
||
### 正确做法
|
||
|
||
```java
|
||
// ✅ 正确示例
|
||
ReactAgent agent1 = ReactAgent.builder()
|
||
.name("agent1")
|
||
.outputKey("agent1_output") // ← 不同的 key
|
||
.build();
|
||
|
||
ReactAgent agent2 = ReactAgent.builder()
|
||
.name("agent2")
|
||
.outputKey("agent2_output") // ← 不同的 key
|
||
.build();
|
||
|
||
// 执行后:
|
||
// state["agent1_output"] = "agent1的输出"
|
||
// state["agent2_output"] = "agent2的输出"
|
||
// 两者都保留!
|
||
```
|
||
|
||
---
|
||
|
||
### 实际案例
|
||
|
||
在 `AiOpsService.java` 中:
|
||
- Planner 用 `"planner_plan"`(第 108 行)
|
||
- Executor 用 `"executor_feedback"`(第 123 行)
|
||
- **绝对不能重复**,否则 Supervisor 无法正确调度
|
||
|
||
**代码证据**:
|
||
```java
|
||
// AiOpsService.java:100-109
|
||
ReactAgent plannerAgent = ReactAgent.builder()
|
||
.name("planner_agent")
|
||
.outputKey("planner_plan") // ← Planner 的 key
|
||
.build();
|
||
|
||
// AiOpsService.java:115-124
|
||
ReactAgent executorAgent = ReactAgent.builder()
|
||
.name("executor_agent")
|
||
.outputKey("executor_feedback") // ← Executor 的 key(不同)
|
||
.build();
|
||
```
|
||
|
||
---
|
||
|
||
### 调试技巧
|
||
|
||
如果怀疑 outputKey 冲突,可以在 Supervisor 调用后打印 state:
|
||
|
||
```java
|
||
Optional<OverAllState> stateOptional = supervisorAgent.invoke(taskPrompt);
|
||
|
||
if (stateOptional.isPresent()) {
|
||
OverAllState state = stateOptional.get();
|
||
logger.debug("State keys: {}", state.keys()); // 查看有哪些 key
|
||
|
||
// 检查是否有意外覆盖
|
||
state.keys().forEach(key -> {
|
||
logger.debug("{} = {}", key, state.value(key));
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## ❓ 疑问 3:如何在 Prompt 中读取多个 key?
|
||
|
||
### 答案
|
||
|
||
直接在 Prompt 中使用**多个 `{}` 占位符**即可
|
||
|
||
---
|
||
|
||
### 示例:读取 3 个 key
|
||
|
||
```java
|
||
// 示例:Planner 需要读取 3 个 key
|
||
private String buildPlannerPrompt() {
|
||
return """
|
||
你是 Planner Agent,负责:
|
||
1. 读取用户任务:{input}
|
||
2. 读取 Executor 的反馈:{executor_feedback}
|
||
3. 读取历史分析记录:{history}
|
||
|
||
根据以上信息,制定下一步计划...
|
||
""";
|
||
}
|
||
|
||
// 执行时自动替换为:
|
||
// 1. 读取用户任务:你是企业级 SRE,接到了...
|
||
// 2. 读取 Executor 的反馈:{"status":"SUCCESS"...}
|
||
// 3. 读取历史分析记录:[上一次分析的内容]
|
||
```
|
||
|
||
---
|
||
|
||
### 实际应用
|
||
|
||
在 `AiOpsService.java:147` 中,Planner 的 Prompt 就读取了 **2 个 key**:
|
||
|
||
```java
|
||
"1. 读取当前输入任务 {input} 以及 Executor 的最近反馈 {executor_feedback}。"
|
||
// ^^^^^^^ ^^^^^^^^^^^^^^^^^^^
|
||
// 第1个key 第2个key
|
||
```
|
||
|
||
**替换后**:
|
||
```
|
||
1. 读取当前输入任务 [taskPrompt的内容] 以及 Executor 的最近反馈 [executor的JSON反馈]。
|
||
```
|
||
|
||
---
|
||
|
||
### 高级技巧:条件读取(模板引擎语法)
|
||
|
||
如果某个 key 可能不存在,可以在 Prompt 中加判断逻辑:
|
||
|
||
```java
|
||
private String buildPlannerPrompt() {
|
||
return """
|
||
你是 Planner Agent,负责:
|
||
1. 读取用户任务:{input}
|
||
|
||
{% if executor_feedback %}
|
||
2. 参考 Executor 的反馈:{executor_feedback}
|
||
{% else %}
|
||
2. 这是第一次规划,没有反馈
|
||
{% endif %}
|
||
""";
|
||
}
|
||
```
|
||
|
||
**注意**:Spring AI Agent Framework 使用的模板引擎(可能是 Freemarker 或 Velocity),具体语法细节需要查阅官方文档。
|
||
|
||
---
|
||
|
||
### 代码证据
|
||
|
||
**Executor Prompt 读取 1 个 key**:
|
||
```java
|
||
// AiOpsService.java:243
|
||
"你是 Executor Agent,负责读取 Planner 最新输出 {planner_plan},只执行其中的第一步。"
|
||
// ^^^^^^^^^^^^^^^
|
||
// 读取 Planner 的输出
|
||
```
|
||
|
||
**Planner Prompt 读取 2 个 key**:
|
||
```java
|
||
// AiOpsService.java:147
|
||
"1. 读取当前输入任务 {input} 以及 Executor 的最近反馈 {executor_feedback}。"
|
||
// ^^^^^^^ ^^^^^^^^^^^^^^^^^^^
|
||
// key1 key2
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 总结
|
||
|
||
| 疑问 | 核心答案 | 关键点 |
|
||
|------|---------|--------|
|
||
| **1. Prompt 占位符如何替换?** | Spring AI 自动从 `state` 中读取 | `{key}` → `state.get("key")` |
|
||
| **2. 两个 Agent 用同一个 outputKey?** | 后者覆盖前者,数据丢失 | 必须保证 outputKey 唯一 |
|
||
| **3. 如何读取多个 key?** | 直接用多个 `{}` 占位符 | 无数量限制,按需引用 |
|
||
|
||
---
|
||
|
||
## 📊 快速参考表
|
||
|
||
### Prompt 占位符替换规则
|
||
|
||
| 占位符 | 替换为 | 代码位置 |
|
||
|--------|--------|---------|
|
||
| `{input}` | `state.value("input")` | Supervisor.invoke(taskPrompt) |
|
||
| `{planner_plan}` | `state.value("planner_plan")` | Planner outputKey |
|
||
| `{executor_feedback}` | `state.value("executor_feedback")` | Executor outputKey |
|
||
|
||
### outputKey 命名规范
|
||
|
||
| 风格 | 示例 | 推荐度 |
|
||
|------|------|--------|
|
||
| `<角色>_<数据类型>` | `planner_plan`, `executor_feedback` | ⭐️⭐️⭐️ 推荐 |
|
||
| `<角色>_output` | `agent1_output`, `agent2_output` | ⭐️⭐️ 可用 |
|
||
| `<数据类型>` | `plan`, `feedback`, `result` | ⭐️ 不推荐(易冲突) |
|
||
| 泛化命名 | `output`, `data`, `result1` | ❌ 避免 |
|
||
|
||
---
|
||
|
||
## 🔗 相关文档
|
||
|
||
- [AI Ops 核心设计 - Essence 报告](./01-AI-Ops-核心设计-Essence报告.md)
|
||
- [outputKey 深度解析](./02-outputKey-深度解析.md)
|
||
|
||
---
|
||
|
||
> 💡 **下一步**:尝试在自己的项目中实现一个简单的 2-Agent 协同(Planner + Executor),验证这些机制。
|