Files
aiagent/智能体聊天助手记忆问题修复.md
2026-01-22 09:59:02 +08:00

345 lines
13 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.
# 智能体聊天助手记忆问题修复文档
## 问题描述
智能聊天助手无法记住用户信息,具体表现为:
1. 第一次对话:用户输入 "我的名字叫老七"
2. 第二次对话:用户输入 "你还记得我的名字吗?"
3. **预期结果**:助手应该回答 "是的,我记得你叫老七"
4. **实际结果**:助手无法记住用户名字,或者回答错误
**额外问题**:助手有时会回复两次相同的消息。
## 问题分析
### 工作流结构
智能聊天助手的工作流包含以下关键节点:
1. **开始节点** (`start-1`) - 接收用户输入
2. **查询记忆节点** (`cache-query`) - 从Redis查询对话历史
3. **合并上下文节点** (`transform-merge`) - 合并用户输入和记忆
4. **意图理解节点** (`llm-intent`) - 分析用户意图
5. **意图路由节点** (`switch-intent`) - 根据意图分发到不同分支
6. **问题回答节点** (`llm-question`) - 生成回答
7. **合并回复节点** (`merge-response`) - 合并各分支结果
8. **更新记忆节点** (`cache-update`) - 更新对话历史到Redis
9. **格式化回复节点** (`llm-format`) - 格式化最终回复
10. **结束节点** (`end-1`) - 返回最终结果
### 根本原因
经过深入调试,发现以下问题:
#### 1. Cache节点数据存储问题
- **问题**`cache-update` 节点的 `value_template` 中,`{{user_input}}``{{output}}``{{timestamp}}` 等变量在Python表达式执行时被当作字符串 `"null"` 处理
- **原因**变量替换逻辑在Python表达式执行之后导致变量未正确替换
#### 2. Cache节点数据读取问题
- **问题**`cache-query` 节点的 `default_value` 中,`conversation_history` 初始化为 `null`,导致后续 `null + [...]` 操作失败
- **原因**JSON解析后空值被解析为 `None`,而不是空列表 `[]`
#### 3. Transform节点数据丢失问题
- **问题**`transform-merge` 节点的 `mapping` 中,`{{output}}` 映射到 `memory` 字段,但 `cache-query` 的输出结构不包含完整的 `memory` 对象
- **原因**`cache-query` 返回的数据结构是 `{"right": {...}}`,而 `transform-merge` 期望的是完整的 `memory` 对象
#### 4. 边连接导致的数据丢失问题
- **问题**`cache-query → transform-merge` 的边设置了 `sourceHandle='right'`,导致只有 `right` 字段被传递,其他内存相关字段丢失
- **原因**`get_node_input` 方法在处理 `sourceHandle` 时,只传递了指定字段,没有保留内存相关字段
#### 5. LLM节点变量替换问题
- **问题1**LLM节点的prompt模板中`{{user_input}}``{{memory.conversation_history}}` 无法正确替换
- **原因**:变量替换逻辑不支持嵌套路径(如 `{{memory.conversation_history}}`
- **问题2**`{{output}}` 变量无法从嵌套的 `right.right.right` 结构中提取
- **原因**:变量提取逻辑只检查顶层字段,没有递归查找
#### 6. 前端重复回复问题
- **问题**:助手有时会回复两次相同的消息
- **原因**:轮询逻辑中,`checkStatus` 函数在状态为 `completed` 时可能被多次调用,导致重复添加消息
## 修复方案
### 1. 修复Cache节点的变量替换逻辑
**文件**`backend/app/services/workflow_engine.py`
**修改位置**`execute_node` 方法中的 `cache` 节点处理逻辑
**关键修改**
```python
# 在Python表达式执行之前先替换变量
if '{{user_input}}' in value_template:
user_input_value = input_data.get('user_input') or input_data.get('query') or input_data.get('input') or input_data.get('USER_INPUT') or ''
user_input_escaped = json_module.dumps(user_input_value, ensure_ascii=False)[1:-1] # 移除外层引号
value_template = value_template.replace('{{user_input}}', user_input_escaped)
if '{{output}}' in value_template:
output_value = self._extract_output_value(input_data)
output_escaped = json_module.dumps(output_value, ensure_ascii=False)[1:-1] # 移除外层引号
value_template = value_template.replace('{{output}}', output_escaped)
if '{{timestamp}}' in value_template:
timestamp_value = input_data.get('timestamp') or datetime.now().isoformat()
timestamp_escaped = json_module.dumps(timestamp_value, ensure_ascii=False)[1:-1] # 移除外层引号
value_template = value_template.replace('{{timestamp}}', timestamp_escaped)
```
**说明**
- 使用 `json.dumps()[1:-1]` 来正确转义字符串,同时移除外层引号
- 在Python表达式执行之前完成变量替换
### 2. 修复Cache节点的默认值初始化
**文件**`backend/app/services/workflow_engine.py`
**修改位置**`execute_node` 方法中的 `cache-query` 处理逻辑
**关键修改**
```python
# 确保default_value中的conversation_history初始化为空列表
default_value_str = node_data.get('default_value', '{}')
try:
default_value = json_module.loads(default_value_str)
# 确保conversation_history是列表而不是null
if 'conversation_history' not in default_value or default_value.get('conversation_history') is None:
default_value['conversation_history'] = []
except:
default_value = {"conversation_history": [], "user_profile": {}, "context": {}}
```
### 3. 修复Transform节点的Memory字段构建
**文件**`backend/app/services/workflow_engine.py`
**修改位置**`execute_node` 方法中的 `transform` 节点处理逻辑(`merge` 模式)
**关键修改**
```python
# 如果memory字段为空尝试从顶层字段构建
if key == 'memory' and (value is None or value == '' or value == '{{output}}'):
# 尝试从expanded_input中构建memory对象
memory = {}
for field in ['conversation_history', 'user_profile', 'context']:
if field in expanded_input:
memory[field] = expanded_input[field]
if memory:
result[key] = memory
else:
# 如果还是找不到保留原有的memory字段如果有
if 'memory' in expanded_input:
result[key] = expanded_input['memory']
else:
result[key] = value
```
### 4. 修复边连接导致的数据丢失
**文件**`backend/app/services/workflow_engine.py`
**修改位置**`get_node_input` 方法
**关键修改**
```python
# 即使有sourceHandle也要保留内存相关字段
if edge.get('sourceHandle'):
input_data[edge['sourceHandle']] = source_output
# 显式保留内存相关字段
if isinstance(source_output, dict):
for field in ['conversation_history', 'user_profile', 'context', 'memory']:
if field in source_output:
input_data[field] = source_output[field]
else:
# ... 原有逻辑
```
### 5. 修复LLM节点的嵌套路径变量支持
**文件**`backend/app/services/workflow_engine.py`
**修改位置**`execute_node` 方法中的 `llm` 节点处理逻辑(变量替换部分)
**关键修改**
```python
# 支持嵌套路径,如 {{memory.conversation_history}}
if '.' in var_name:
value = self._get_nested_value(input_data, var_name)
else:
# 原有逻辑:检查别名和直接字段
value = input_data.get(var_name)
if value is None:
# 检查别名
aliases = {
'user_input': ['query', 'input', 'USER_INPUT', 'user_input'],
'output': ['result', 'response', 'text', 'content']
}
for alias_key, alias_list in aliases.items():
if var_name == alias_key:
for alias in alias_list:
value = input_data.get(alias)
if value is not None:
break
```
**特殊处理**`{{memory.conversation_history}}` 格式化
```python
# 如果是conversation_history格式化为可读格式
if var_name == 'memory.conversation_history' and isinstance(value, list):
formatted_history = []
for msg in value:
role = msg.get('role', 'unknown')
content = msg.get('content', '')
if role == 'user':
formatted_history.append(f"用户: {content}")
elif role == 'assistant':
formatted_history.append(f"助手: {content}")
value = '\n'.join(formatted_history) if formatted_history else '无对话历史'
```
### 6. 修复{{output}}变量的递归提取
**文件**`backend/app/services/workflow_engine.py`
**修改位置**`execute_node` 方法中的 `llm` 节点处理逻辑(`{{output}}` 变量处理)
**关键修改**
```python
# 特殊处理output变量递归查找right字段
if var_path == 'output':
right_value = input_data.get('right')
if right_value:
# 递归查找字符串值
def extract_string_from_right(obj, depth=0):
if isinstance(obj, str):
return obj
elif isinstance(obj, dict):
# 优先检查常见字段
for key in ['content', 'text', 'message', 'output']:
if key in obj and isinstance(obj[key], str):
return obj[key]
# 递归查找right字段
if 'right' in obj:
return extract_string_from_right(obj['right'], depth + 1)
return None
value = extract_string_from_right(right_value)
if value:
logger.info(f"[rjb] LLM节点从right字段提取output: {value[:100]}")
```
### 7. 修复前端重复回复问题
**文件**`frontend/src/components/AgentChatPreview.vue`
**修改位置**`handleSendMessage` 方法中的 `checkStatus` 函数
**关键修改**
```typescript
// 添加标志位,防止重复添加回复
let replyAdded = false
const checkStatus = async () => {
try {
// 如果已经添加过回复,直接返回
if (replyAdded) {
return
}
// ... 获取执行状态 ...
if (exec.status === 'completed') {
// 防止重复添加
if (replyAdded) {
return
}
// 标记已添加回复
replyAdded = true
// 添加回复消息
messages.value.push({
role: 'agent',
content: agentReply || '执行完成',
timestamp: Date.now()
})
// ... 其他逻辑 ...
}
} catch (error) {
// 同样添加防重复逻辑
if (replyAdded) {
return
}
replyAdded = true
// ... 错误处理 ...
}
}
// 每次发送新消息时重置标志位
replyAdded = false
```
## 测试验证
### 测试步骤
1. **第一次对话**
- 输入:`我的名字叫老七`
- 预期:助手正常回复,并将信息存储到记忆
2. **第二次对话**
- 输入:`你还记得我的名字吗?`
- 预期:助手回答 `是的,我记得你叫老七`
3. **验证重复回复**
- 观察是否只收到一次回复
### 测试结果
**记忆功能**:正常工作,能正确记住用户名字
**重复回复**:已修复,每次对话只回复一次
## 关键代码文件
1. **后端核心逻辑**
- `backend/app/services/workflow_engine.py` - 工作流执行引擎
- `backend/scripts/generate_chat_agent.py` - 智能聊天助手工作流定义
2. **前端组件**
- `frontend/src/components/AgentChatPreview.vue` - 聊天预览组件
3. **测试脚本**
- `test_memory_functionality.py` - 记忆功能测试脚本
- `test_output_variable_extraction.py` - 输出变量提取测试脚本
## 修复时间线
1. **问题发现**:用户报告无法记住名字
2. **初步分析**:检查工作流定义和节点配置
3. **深入调试**:添加详细日志,追踪数据流
4. **修复Cache节点**:修复数据存储和读取逻辑
5. **修复Transform节点**:修复数据合并逻辑
6. **修复LLM节点**:修复变量替换和嵌套路径支持
7. **修复前端**:修复重复回复问题
8. **测试验证**:确认所有问题已解决
## 经验总结
1. **数据流追踪**:在复杂工作流中,需要仔细追踪数据在每个节点之间的传递
2. **变量替换时机**:变量替换必须在表达式执行之前完成
3. **数据结构一致性**:确保上下游节点对数据结构的期望一致
4. **边界情况处理**:注意处理 `null`、空值、嵌套结构等边界情况
5. **前端防重复**:轮询逻辑中需要添加防重复机制
## 后续优化建议
1. **统一数据结构**:定义统一的数据结构规范,避免节点间数据格式不一致
2. **增强日志**:添加更详细的调试日志,方便问题排查
3. **单元测试**:为关键节点添加单元测试,确保修复的稳定性
4. **性能优化**优化轮询频率减少不必要的API调用
5. **错误处理**:增强错误处理机制,提供更友好的错误提示
---
**修复完成时间**2024年根据实际时间填写
**修复人员**AI Assistant
**文档版本**v1.0