345 lines
13 KiB
Markdown
345 lines
13 KiB
Markdown
# 智能体聊天助手记忆问题修复文档
|
||
|
||
## 问题描述
|
||
|
||
智能聊天助手无法记住用户信息,具体表现为:
|
||
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
|