Files
aiagent/智能体聊天助手记忆问题修复.md

345 lines
13 KiB
Markdown
Raw Permalink Normal View History

2026-01-22 09:59:02 +08:00
# 智能体聊天助手记忆问题修复文档
## 问题描述
智能聊天助手无法记住用户信息,具体表现为:
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