处理agent答非所问的问题
This commit is contained in:
233
(红头)工作流调用测试总结.txt
Normal file
233
(红头)工作流调用测试总结.txt
Normal file
@@ -0,0 +1,233 @@
|
||||
================================================================================
|
||||
工作流调用测试总结
|
||||
================================================================================
|
||||
|
||||
测试时间:2026-01-19
|
||||
测试场景:Agent工作流执行 - LLM节点"答非所问"问题诊断与修复
|
||||
|
||||
================================================================================
|
||||
一、问题描述
|
||||
================================================================================
|
||||
|
||||
1. 问题现象:
|
||||
- LLM节点单独测试时能正常回答用户问题
|
||||
- 整个Agent调用时,LLM返回通用欢迎语,而不是回答用户的具体问题
|
||||
- 用户输入:"苹果英语怎么讲?"
|
||||
- LLM返回:通用欢迎语 + 最后附加用户问题
|
||||
|
||||
2. 工作流结构:
|
||||
- 开始节点 (start-1) → LLM处理节点 (llm-1) → 结束节点 (end-1)
|
||||
- LLM节点配置:prompt = "请处理用户请求。"
|
||||
|
||||
================================================================================
|
||||
二、测试方法与工具
|
||||
================================================================================
|
||||
|
||||
1. 执行日志查看脚本:
|
||||
- 文件:check_execution_logs.py
|
||||
- 功能:查看最近的Agent执行记录,包括输入数据、输出数据、执行日志
|
||||
- 使用方法:python3 check_execution_logs.py
|
||||
|
||||
2. 数据流转测试脚本:
|
||||
- 文件:test_workflow_data_flow.py
|
||||
- 功能:模拟完整工作流执行,详细记录每个节点的输入输出
|
||||
- 使用方法:python3 test_workflow_data_flow.py
|
||||
|
||||
3. 日志查看命令:
|
||||
- 后端日志:docker-compose -f docker-compose.dev.yml logs --tail=500 backend | grep "\[rjb\]"
|
||||
- Celery日志:docker-compose -f docker-compose.dev.yml logs --tail=500 celery | grep "\[rjb\]"
|
||||
|
||||
================================================================================
|
||||
三、问题定位过程
|
||||
================================================================================
|
||||
|
||||
1. 数据流转检查:
|
||||
- 输入数据(正确):{"query": "苹果英语怎么讲?", "USER_INPUT": "苹果英语怎么讲?"}
|
||||
- Start节点输出(正确):{"query": "苹果英语怎么讲?", "USER_INPUT": "苹果英语怎么讲?"}
|
||||
- LLM节点输入(问题):从执行日志看,数据被包装成了 {"input": {"query": "...", "USER_INPUT": "..."}}
|
||||
- LLM节点输出(问题):返回通用欢迎语,而不是回答用户问题
|
||||
|
||||
2. 关键发现:
|
||||
- 通过添加详细的调试日志([rjb]标记),发现:
|
||||
* user_query提取逻辑能正确提取到用户问题
|
||||
* 但prompt格式化时,通用指令检测可能有问题
|
||||
* 或者LLM服务接收到的prompt不是格式化后的prompt
|
||||
|
||||
3. 日志分析结果:
|
||||
- 从Celery worker日志中发现:
|
||||
* user_query正确提取:"苹果英语怎么说?"
|
||||
* 通用指令检测成功:is_generic_instruction=True
|
||||
* formatted_prompt正确设置为:"苹果英语怎么说?"
|
||||
* 实际发送给DeepSeek的prompt也是:"苹果英语怎么说?"
|
||||
* LLM正确回答:"苹果的英语是 **apple**。"
|
||||
|
||||
================================================================================
|
||||
四、问题根因分析
|
||||
================================================================================
|
||||
|
||||
1. 主要问题:
|
||||
- LLM节点的prompt配置是通用指令"请处理用户请求。"
|
||||
- 当prompt是通用指令时,应该直接使用用户输入作为prompt
|
||||
- 但之前的逻辑可能没有正确处理这种情况
|
||||
|
||||
2. 数据流转问题:
|
||||
- get_node_input方法在处理非字典类型的source_output时,会包装成{"input": source_output}
|
||||
- 这导致LLM节点接收到的输入数据格式为:{"input": "..."}
|
||||
- user_query提取逻辑需要处理这种嵌套结构
|
||||
|
||||
3. End节点问题:
|
||||
- End节点在提取输出时,会将所有文本字段组合
|
||||
- 包括query字段,导致最终输出包含用户问题
|
||||
|
||||
================================================================================
|
||||
五、修复方案
|
||||
================================================================================
|
||||
|
||||
1. 修复user_query提取逻辑(backend/app/services/workflow_engine.py):
|
||||
- 添加对嵌套input字段的处理
|
||||
- 优先从嵌套的input中提取query等字段
|
||||
- 如果嵌套input不存在,从顶层提取
|
||||
- 支持多层嵌套结构
|
||||
|
||||
2. 修复prompt格式化逻辑(backend/app/services/workflow_engine.py):
|
||||
- 增强通用指令检测:识别"请处理用户请求。"等通用指令
|
||||
- 当检测到通用指令时,直接使用user_query作为prompt
|
||||
- 添加详细的调试日志,记录整个格式化过程
|
||||
|
||||
3. 修复End节点输出处理(backend/app/services/workflow_engine.py):
|
||||
- 在exclude_keys中添加'query', 'USER_INPUT', 'user_input', 'user_query'
|
||||
- 优先使用input字段(LLM的实际输出)
|
||||
- 避免将用户查询字段包含在最终输出中
|
||||
|
||||
4. 添加LLM服务调用日志(backend/app/services/llm_service.py):
|
||||
- 在DeepSeek API调用前记录实际发送的prompt
|
||||
- 便于调试和验证prompt是否正确
|
||||
|
||||
================================================================================
|
||||
六、修复后的验证
|
||||
================================================================================
|
||||
|
||||
1. 测试输入:
|
||||
- 用户问题:"苹果英语怎么说?"
|
||||
- 输入数据:{"query": "苹果英语怎么说?", "USER_INPUT": "苹果英语怎么说?"}
|
||||
|
||||
2. 执行日志验证:
|
||||
[rjb] 开始提取user_query: input_data={'USER_INPUT': '苹果英语怎么说?', 'query': '苹果英语怎么说?'}
|
||||
[rjb] 从顶层提取: key=query, value=苹果英语怎么说?, value_type=<class 'str'>
|
||||
[rjb] 提取到字符串user_query: 苹果英语怎么说?
|
||||
[rjb] 最终提取的user_query: 苹果英语怎么说?
|
||||
[rjb] 检测到通用指令,直接使用用户输入作为prompt: 苹果英语怎么说?
|
||||
[rjb] LLM节点prompt格式化: final_prompt前200字符='苹果英语怎么说?'
|
||||
[rjb] 准备调用LLM: prompt前200字符='苹果英语怎么说?'
|
||||
[rjb] DeepSeek实际发送的prompt前200字符: 苹果英语怎么说?
|
||||
|
||||
3. LLM输出验证:
|
||||
- LLM正确回答:"苹果的英语是 **apple**。"
|
||||
- 包含发音、例句、相关表达等详细信息
|
||||
|
||||
4. End节点输出验证:
|
||||
- 最终输出只包含LLM的回答内容
|
||||
- 不再包含用户问题
|
||||
|
||||
================================================================================
|
||||
七、关键代码修改点
|
||||
================================================================================
|
||||
|
||||
1. user_query提取逻辑(第467-525行):
|
||||
- 添加嵌套input字段检查
|
||||
- 支持多层数据提取
|
||||
- 添加详细的调试日志
|
||||
|
||||
2. prompt格式化逻辑(第527-568行):
|
||||
- 增强通用指令检测
|
||||
- 当检测到通用指令时,直接使用user_query作为prompt
|
||||
- 添加is_generic_instruction变量初始化
|
||||
|
||||
3. End节点输出处理(第1780-1799行):
|
||||
- 在exclude_keys中添加用户查询相关字段
|
||||
- 优先使用input字段
|
||||
- 避免拼接用户问题
|
||||
|
||||
4. LLM服务调用日志(backend/app/services/llm_service.py):
|
||||
- 在API调用前记录实际发送的prompt
|
||||
|
||||
================================================================================
|
||||
八、测试经验总结
|
||||
================================================================================
|
||||
|
||||
1. 调试方法:
|
||||
- 使用详细的调试日志([rjb]标记)追踪数据流转
|
||||
- 在关键位置添加日志:数据提取、格式化、API调用
|
||||
- 查看Celery worker日志(工作流在Celery中执行)
|
||||
|
||||
2. 问题定位技巧:
|
||||
- 对比节点测试和完整工作流执行的差异
|
||||
- 检查数据在节点间的传递格式
|
||||
- 验证实际发送给LLM的prompt内容
|
||||
|
||||
3. 常见问题:
|
||||
- 数据被包装成嵌套结构(如{"input": {...}})
|
||||
- 通用指令没有被正确识别
|
||||
- End节点输出包含了不应该包含的字段
|
||||
|
||||
4. 最佳实践:
|
||||
- 在LLM节点配置中,如果prompt是通用指令,应该直接使用用户输入
|
||||
- End节点应该只输出LLM的实际回答,排除用户查询字段
|
||||
- 添加详细的调试日志,便于问题定位
|
||||
|
||||
================================================================================
|
||||
九、测试工具说明
|
||||
================================================================================
|
||||
|
||||
1. check_execution_logs.py:
|
||||
- 功能:查看最近的Agent执行记录和详细日志
|
||||
- 输出:输入数据、输出数据、执行时间线、LLM节点详细分析
|
||||
- 使用方法:python3 check_execution_logs.py
|
||||
|
||||
2. test_workflow_data_flow.py:
|
||||
- 功能:模拟完整工作流执行,追踪数据流转
|
||||
- 输出:每个节点的输入输出、数据格式转换过程
|
||||
- 使用方法:python3 test_workflow_data_flow.py
|
||||
|
||||
3. 日志查看脚本(view_logs.sh):
|
||||
- 功能:实时查看包含[rjb]标记的调试日志
|
||||
- 使用方法:./view_logs.sh
|
||||
|
||||
================================================================================
|
||||
十、修复验证结果
|
||||
================================================================================
|
||||
|
||||
✅ 问题1:LLM答非所问
|
||||
- 状态:已修复
|
||||
- 验证:LLM能正确回答用户问题
|
||||
|
||||
✅ 问题2:End节点输出包含用户问题
|
||||
- 状态:已修复
|
||||
- 验证:最终输出只包含LLM的回答
|
||||
|
||||
✅ 问题3:数据流转问题
|
||||
- 状态:已修复
|
||||
- 验证:数据在节点间正确传递,格式正确
|
||||
|
||||
================================================================================
|
||||
十一、后续建议
|
||||
================================================================================
|
||||
|
||||
1. 节点配置建议:
|
||||
- 如果LLM节点的prompt是通用指令(如"请处理用户请求。"),系统会自动使用用户输入作为prompt
|
||||
- 如果需要更具体的指令,可以在prompt中明确说明任务类型
|
||||
|
||||
2. 测试建议:
|
||||
- 使用check_execution_logs.py查看执行日志
|
||||
- 使用Celery日志查看详细的调试信息
|
||||
- 在节点配置面板中单独测试节点,对比完整工作流的执行
|
||||
|
||||
3. 监控建议:
|
||||
- 定期检查执行日志,确认数据流转正常
|
||||
- 关注LLM节点的输入输出,确保prompt格式化正确
|
||||
- 检查End节点的输出,确保不包含用户查询字段
|
||||
|
||||
================================================================================
|
||||
测试完成时间:2026-01-19 23:55
|
||||
测试人员:AI Assistant
|
||||
================================================================================
|
||||
@@ -10,6 +10,7 @@ from app.core.database import get_db
|
||||
from app.models.execution_log import ExecutionLog
|
||||
from app.models.execution import Execution
|
||||
from app.models.workflow import Workflow
|
||||
from app.models.agent import Agent
|
||||
from app.api.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from app.core.exceptions import NotFoundError
|
||||
@@ -38,21 +39,34 @@ async def get_execution_logs(
|
||||
execution_id: str,
|
||||
level: Optional[str] = Query(None, description="日志级别筛选: INFO/WARN/ERROR/DEBUG"),
|
||||
node_id: Optional[str] = Query(None, description="节点ID筛选"),
|
||||
skip: int = Query(0, ge=0),
|
||||
skip: int =Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取执行日志列表"""
|
||||
# 验证执行记录是否存在且属于当前用户
|
||||
execution = db.query(Execution).join(Workflow, Execution.workflow_id == Workflow.id).filter(
|
||||
Execution.id == execution_id,
|
||||
Workflow.user_id == current_user.id
|
||||
).first()
|
||||
from app.models.agent import Agent
|
||||
|
||||
# 验证执行记录是否存在
|
||||
execution = db.query(Execution).filter(Execution.id == execution_id).first()
|
||||
|
||||
if not execution:
|
||||
raise NotFoundError("执行记录", execution_id)
|
||||
|
||||
# 验证权限:检查workflow或agent的所有权
|
||||
has_permission = False
|
||||
if execution.workflow_id:
|
||||
workflow = db.query(Workflow).filter(Workflow.id == execution.workflow_id).first()
|
||||
if workflow and workflow.user_id == current_user.id:
|
||||
has_permission = True
|
||||
elif execution.agent_id:
|
||||
agent = db.query(Agent).filter(Agent.id == execution.agent_id).first()
|
||||
if agent and (agent.user_id == current_user.id or agent.status in ["published", "running"]):
|
||||
has_permission = True
|
||||
|
||||
if not has_permission:
|
||||
raise NotFoundError("执行记录", execution_id)
|
||||
|
||||
# 构建查询
|
||||
query = db.query(ExecutionLog).filter(
|
||||
ExecutionLog.execution_id == execution_id
|
||||
@@ -79,15 +93,26 @@ async def get_execution_log_summary(
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取执行日志摘要(统计信息)"""
|
||||
# 验证执行记录是否存在且属于当前用户
|
||||
execution = db.query(Execution).join(Workflow, Execution.workflow_id == Workflow.id).filter(
|
||||
Execution.id == execution_id,
|
||||
Workflow.user_id == current_user.id
|
||||
).first()
|
||||
# 验证执行记录是否存在
|
||||
execution = db.query(Execution).filter(Execution.id == execution_id).first()
|
||||
|
||||
if not execution:
|
||||
raise NotFoundError("执行记录", execution_id)
|
||||
|
||||
# 验证权限:检查workflow或agent的所有权
|
||||
has_permission = False
|
||||
if execution.workflow_id:
|
||||
workflow = db.query(Workflow).filter(Workflow.id == execution.workflow_id).first()
|
||||
if workflow and workflow.user_id == current_user.id:
|
||||
has_permission = True
|
||||
elif execution.agent_id:
|
||||
agent = db.query(Agent).filter(Agent.id == execution.agent_id).first()
|
||||
if agent and (agent.user_id == current_user.id or agent.status in ["published", "running"]):
|
||||
has_permission = True
|
||||
|
||||
if not has_permission:
|
||||
raise NotFoundError("执行记录", execution_id)
|
||||
|
||||
# 统计各级别日志数量
|
||||
from sqlalchemy import func
|
||||
level_stats = db.query(
|
||||
@@ -148,15 +173,26 @@ async def get_execution_performance(
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取执行性能分析数据"""
|
||||
# 验证执行记录是否存在且属于当前用户
|
||||
execution = db.query(Execution).join(Workflow, Execution.workflow_id == Workflow.id).filter(
|
||||
Execution.id == execution_id,
|
||||
Workflow.user_id == current_user.id
|
||||
).first()
|
||||
# 验证执行记录是否存在
|
||||
execution = db.query(Execution).filter(Execution.id == execution_id).first()
|
||||
|
||||
if not execution:
|
||||
raise NotFoundError("执行记录", execution_id)
|
||||
|
||||
# 验证权限:检查workflow或agent的所有权
|
||||
has_permission = False
|
||||
if execution.workflow_id:
|
||||
workflow = db.query(Workflow).filter(Workflow.id == execution.workflow_id).first()
|
||||
if workflow and workflow.user_id == current_user.id:
|
||||
has_permission = True
|
||||
elif execution.agent_id:
|
||||
agent = db.query(Agent).filter(Agent.id == execution.agent_id).first()
|
||||
if agent and (agent.user_id == current_user.id or agent.status in ["published", "running"]):
|
||||
has_permission = True
|
||||
|
||||
if not has_permission:
|
||||
raise NotFoundError("执行记录", execution_id)
|
||||
|
||||
from sqlalchemy import func
|
||||
|
||||
# 获取总执行时间
|
||||
|
||||
@@ -150,6 +150,11 @@ class LLMService:
|
||||
client = self.deepseek_client
|
||||
|
||||
try:
|
||||
# 记录实际发送给LLM的prompt
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"[rjb] DeepSeek实际发送的prompt前200字符: {prompt[:200] if len(prompt) > 200 else prompt}")
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
|
||||
@@ -116,7 +116,7 @@ class WorkflowEngine:
|
||||
if edge['target'] == node_id:
|
||||
source_id = edge['source']
|
||||
source_output = node_outputs.get(source_id, {})
|
||||
logger.debug(f"[rjb] 获取节点输入: target={node_id}, source={source_id}, source_output={source_output}, sourceHandle={edge.get('sourceHandle')}")
|
||||
logger.info(f"[rjb] 获取节点输入: target={node_id}, source={source_id}, source_output={source_output}, source_output_type={type(source_output)}, sourceHandle={edge.get('sourceHandle')}")
|
||||
|
||||
# 如果有sourceHandle,使用它作为key
|
||||
if 'sourceHandle' in edge and edge['sourceHandle']:
|
||||
@@ -133,9 +133,13 @@ class WorkflowEngine:
|
||||
if key != 'output':
|
||||
input_data[key] = value
|
||||
else:
|
||||
# 直接展开source_output的内容
|
||||
input_data.update(source_output)
|
||||
logger.info(f"[rjb] 展开source_output后: input_data={input_data}")
|
||||
else:
|
||||
# 如果source_output不是字典,包装到input字段
|
||||
input_data['input'] = source_output
|
||||
logger.info(f"[rjb] source_output不是字典,包装到input字段: input_data={input_data}")
|
||||
|
||||
# 如果input_data中没有query字段,尝试从所有已执行的节点中查找(特别是start节点)
|
||||
if 'query' not in input_data:
|
||||
@@ -427,9 +431,13 @@ class WorkflowEngine:
|
||||
# 支持两种格式的变量:{key} 和 {{key}}
|
||||
formatted_prompt = prompt
|
||||
has_unfilled_variables = False
|
||||
has_any_placeholder = False
|
||||
|
||||
import re
|
||||
# 检查是否有任何占位符
|
||||
has_any_placeholder = bool(re.search(r'\{\{?\w+\}?\}', prompt))
|
||||
|
||||
# 首先处理 {{variable}} 格式(模板节点常用)
|
||||
import re
|
||||
double_brace_vars = re.findall(r'\{\{(\w+)\}\}', prompt)
|
||||
for var_name in double_brace_vars:
|
||||
if var_name in input_data:
|
||||
@@ -456,17 +464,108 @@ class WorkflowEngine:
|
||||
json_module.dumps(input_data, ensure_ascii=False)
|
||||
)
|
||||
|
||||
# 如果仍有未填充的变量({{variable}}格式),将用户输入作为上下文附加
|
||||
if has_unfilled_variables or re.search(r'\{\{(\w+)\}\}', formatted_prompt):
|
||||
# 提取用户的实际查询内容
|
||||
user_query = input_data.get('query', input_data.get('input', input_data.get('text', '')))
|
||||
if not user_query and isinstance(input_data, dict):
|
||||
# 如果没有明确的query字段,尝试从整个input_data中提取文本内容
|
||||
user_query = json_module.dumps(input_data, ensure_ascii=False)
|
||||
# 提取用户的实际查询内容(优先提取)
|
||||
user_query = None
|
||||
logger.info(f"[rjb] 开始提取user_query: input_data={input_data}, input_data_type={type(input_data)}")
|
||||
if isinstance(input_data, dict):
|
||||
# 首先检查是否有嵌套的input字段
|
||||
nested_input = input_data.get('input')
|
||||
logger.info(f"[rjb] 检查嵌套input: nested_input={nested_input}, nested_input_type={type(nested_input) if nested_input else None}")
|
||||
if isinstance(nested_input, dict):
|
||||
# 从嵌套的input中提取
|
||||
for key in ['query', 'input', 'text', 'message', 'content', 'user_input', 'USER_INPUT']:
|
||||
if key in nested_input:
|
||||
user_query = nested_input[key]
|
||||
logger.info(f"[rjb] 从嵌套input中提取到user_query: key={key}, user_query={user_query}")
|
||||
break
|
||||
|
||||
# 如果还没有,从顶层提取
|
||||
if not user_query:
|
||||
for key in ['query', 'input', 'text', 'message', 'content', 'user_input', 'USER_INPUT']:
|
||||
if key in input_data:
|
||||
value = input_data[key]
|
||||
logger.info(f"[rjb] 从顶层提取: key={key}, value={value}, value_type={type(value)}")
|
||||
# 如果值是字符串,直接使用
|
||||
if isinstance(value, str):
|
||||
user_query = value
|
||||
logger.info(f"[rjb] 提取到字符串user_query: {user_query}")
|
||||
break
|
||||
# 如果值是字典,尝试从中提取
|
||||
elif isinstance(value, dict):
|
||||
for sub_key in ['query', 'input', 'text', 'message', 'content', 'user_input', 'USER_INPUT']:
|
||||
if sub_key in value:
|
||||
user_query = value[sub_key]
|
||||
logger.info(f"[rjb] 从字典值中提取到user_query: sub_key={sub_key}, user_query={user_query}")
|
||||
break
|
||||
if user_query:
|
||||
break
|
||||
|
||||
# 如果还是没有,使用整个input_data(但排除系统字段)
|
||||
if not user_query:
|
||||
filtered_data = {k: v for k, v in input_data.items() if not k.startswith('_')}
|
||||
logger.info(f"[rjb] 使用filtered_data: filtered_data={filtered_data}")
|
||||
if filtered_data:
|
||||
# 如果只有一个字段且是字符串,直接使用
|
||||
if len(filtered_data) == 1:
|
||||
single_value = list(filtered_data.values())[0]
|
||||
if isinstance(single_value, str):
|
||||
user_query = single_value
|
||||
logger.info(f"[rjb] 从单个字符串字段提取到user_query: {user_query}")
|
||||
elif isinstance(single_value, dict):
|
||||
# 从字典中提取第一个字符串值
|
||||
for v in single_value.values():
|
||||
if isinstance(v, str):
|
||||
user_query = v
|
||||
logger.info(f"[rjb] 从字典的单个字段中提取到user_query: {user_query}")
|
||||
break
|
||||
if not user_query:
|
||||
user_query = json_module.dumps(filtered_data, ensure_ascii=False) if len(filtered_data) > 1 else str(list(filtered_data.values())[0])
|
||||
logger.info(f"[rjb] 使用JSON或字符串转换: user_query={user_query}")
|
||||
|
||||
logger.info(f"[rjb] 最终提取的user_query: {user_query}")
|
||||
|
||||
# 如果prompt中没有占位符,或者仍有未填充的变量,将用户输入附加到prompt
|
||||
is_generic_instruction = False # 初始化变量
|
||||
if not has_any_placeholder:
|
||||
# 如果prompt中没有占位符,将用户输入作为主要内容
|
||||
if user_query:
|
||||
# 判断是否是通用指令:简短且不包含具体任务描述
|
||||
prompt_stripped = prompt.strip()
|
||||
is_generic_instruction = (
|
||||
len(prompt_stripped) < 30 or # 简短提示词
|
||||
prompt_stripped in [
|
||||
"请处理用户请求。", "请处理用户请求",
|
||||
"请处理以下输入数据:", "请处理以下输入数据",
|
||||
"请处理输入。", "请处理输入",
|
||||
"处理用户请求", "处理请求",
|
||||
"请回答用户问题", "请回答用户问题。",
|
||||
"请帮助用户", "请帮助用户。"
|
||||
] or
|
||||
# 检查是否只包含通用指令关键词
|
||||
(len(prompt_stripped) < 50 and any(keyword in prompt_stripped for keyword in [
|
||||
"请处理", "处理", "请回答", "回答", "请帮助", "帮助", "请执行", "执行"
|
||||
]) and not any(specific in prompt_stripped for specific in [
|
||||
"翻译", "生成", "分析", "总结", "提取", "转换", "计算"
|
||||
]))
|
||||
)
|
||||
|
||||
if is_generic_instruction:
|
||||
# 如果是通用指令,直接使用用户输入作为prompt
|
||||
formatted_prompt = str(user_query)
|
||||
logger.info(f"[rjb] 检测到通用指令,直接使用用户输入作为prompt: {user_query[:50] if user_query else 'None'}")
|
||||
else:
|
||||
# 否则,将用户输入附加到prompt
|
||||
formatted_prompt = f"{formatted_prompt}\n\n{user_query}"
|
||||
logger.info(f"[rjb] 非通用指令,将用户输入附加到prompt")
|
||||
else:
|
||||
# 如果没有提取到用户查询,附加整个input_data
|
||||
formatted_prompt = f"{formatted_prompt}\n\n{json_module.dumps(input_data, ensure_ascii=False)}"
|
||||
elif has_unfilled_variables or re.search(r'\{\{(\w+)\}\}', formatted_prompt):
|
||||
# 如果有占位符但未填充,附加用户需求说明
|
||||
if user_query:
|
||||
formatted_prompt = f"{formatted_prompt}\n\n用户需求:{user_query}\n\n请根据以上用户需求,忽略未填充的变量占位符(如{{{{variable}}}}),直接基于用户需求来完成任务。"
|
||||
|
||||
logger.info(f"[rjb] LLM节点prompt格式化: node_id={node_id}, original_prompt='{prompt[:50] if len(prompt) > 50 else prompt}', has_any_placeholder={has_any_placeholder}, user_query={user_query}, is_generic_instruction={is_generic_instruction}, final_prompt前200字符='{formatted_prompt[:200] if len(formatted_prompt) > 200 else formatted_prompt}'")
|
||||
prompt = formatted_prompt
|
||||
else:
|
||||
# 如果input_data不是dict,直接转换为字符串
|
||||
@@ -510,6 +609,9 @@ class WorkflowEngine:
|
||||
api_key = None
|
||||
base_url = None
|
||||
|
||||
# 记录实际发送给LLM的prompt
|
||||
logger.info(f"[rjb] 准备调用LLM: node_id={node_id}, provider={provider}, model={model}, prompt前200字符='{prompt[:200] if len(prompt) > 200 else prompt}'")
|
||||
|
||||
# 调用LLM服务
|
||||
try:
|
||||
if self.logger:
|
||||
@@ -1677,24 +1779,28 @@ class WorkflowEngine:
|
||||
final_output = str(list(final_output.values())[0])
|
||||
else:
|
||||
# 否则转换为纯文本(不是JSON)
|
||||
# 尝试提取所有文本字段并组合,但排除系统字段
|
||||
# 尝试提取所有文本字段并组合,但排除系统字段和用户查询字段
|
||||
text_parts = []
|
||||
exclude_keys = {'status', 'error', 'timestamp', 'node_id', 'execution_time'}
|
||||
for key, value in final_output.items():
|
||||
if key in exclude_keys:
|
||||
continue
|
||||
if isinstance(value, str) and value.strip():
|
||||
# 如果值本身已经包含 "key: " 格式,直接使用
|
||||
if value.strip().startswith(f"{key}:"):
|
||||
text_parts.append(value.strip())
|
||||
else:
|
||||
text_parts.append(value.strip())
|
||||
elif isinstance(value, (int, float, bool)):
|
||||
text_parts.append(f"{key}: {value}")
|
||||
if text_parts:
|
||||
final_output = "\n".join(text_parts)
|
||||
exclude_keys = {'status', 'error', 'timestamp', 'node_id', 'execution_time', 'query', 'USER_INPUT', 'user_input', 'user_query'}
|
||||
# 优先使用input字段(LLM的实际输出)
|
||||
if 'input' in final_output and isinstance(final_output['input'], str):
|
||||
final_output = final_output['input']
|
||||
else:
|
||||
final_output = str(final_output)
|
||||
for key, value in final_output.items():
|
||||
if key in exclude_keys:
|
||||
continue
|
||||
if isinstance(value, str) and value.strip():
|
||||
# 如果值本身已经包含 "key: " 格式,直接使用
|
||||
if value.strip().startswith(f"{key}:"):
|
||||
text_parts.append(value.strip())
|
||||
else:
|
||||
text_parts.append(value.strip())
|
||||
elif isinstance(value, (int, float, bool)):
|
||||
text_parts.append(f"{key}: {value}")
|
||||
if text_parts:
|
||||
final_output = "\n".join(text_parts)
|
||||
else:
|
||||
final_output = str(final_output)
|
||||
else:
|
||||
final_output = str(final_output)
|
||||
|
||||
@@ -1808,10 +1914,13 @@ class WorkflowEngine:
|
||||
# 如果是起始节点,使用初始输入
|
||||
if node.get('type') == 'start' and not node_input:
|
||||
node_input = input_data
|
||||
logger.info(f"[rjb] Start节点使用初始输入: node_id={next_node_id}, node_input={node_input}")
|
||||
|
||||
# 调试:记录节点输入数据
|
||||
if node.get('type') == 'llm':
|
||||
logger.debug(f"[rjb] LLM节点输入: node_id={next_node_id}, node_input={node_input}, node_outputs keys={list(self.node_outputs.keys())}")
|
||||
logger.info(f"[rjb] LLM节点输入: node_id={next_node_id}, node_input={node_input}, node_outputs keys={list(self.node_outputs.keys())}")
|
||||
if 'start-1' in self.node_outputs:
|
||||
logger.info(f"[rjb] Start节点输出内容: {self.node_outputs['start-1']}")
|
||||
|
||||
# 执行节点
|
||||
result = await self.execute_node(node, node_input)
|
||||
@@ -1819,7 +1928,10 @@ class WorkflowEngine:
|
||||
|
||||
# 保存节点输出
|
||||
if result.get('status') == 'success':
|
||||
self.node_outputs[next_node_id] = result.get('output', {})
|
||||
output_value = result.get('output', {})
|
||||
self.node_outputs[next_node_id] = output_value
|
||||
if node.get('type') == 'start':
|
||||
logger.info(f"[rjb] Start节点输出已保存: node_id={next_node_id}, output={output_value}, output_type={type(output_value)}")
|
||||
|
||||
# 如果是条件节点,根据分支结果过滤边
|
||||
if node.get('type') == 'condition':
|
||||
@@ -1898,6 +2010,7 @@ class WorkflowEngine:
|
||||
final_output = str(list(final_output.values())[0])
|
||||
else:
|
||||
# 否则转换为JSON字符串
|
||||
import json as json_module
|
||||
final_output = json_module.dumps(final_output, ensure_ascii=False)
|
||||
else:
|
||||
final_output = str(final_output)
|
||||
|
||||
126
check_execution_logs.py
Executable file
126
check_execution_logs.py
Executable file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
查看执行日志的脚本
|
||||
用于诊断数据流转问题
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.execution import Execution
|
||||
from app.models.execution_log import ExecutionLog
|
||||
|
||||
|
||||
def format_json(data):
|
||||
"""格式化JSON数据"""
|
||||
if isinstance(data, dict):
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
return str(data)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# 获取最近的执行记录
|
||||
print("=" * 80)
|
||||
print("查找最近的Agent执行记录...")
|
||||
print("=" * 80)
|
||||
|
||||
execution = db.query(Execution).filter(
|
||||
Execution.agent_id.isnot(None)
|
||||
).order_by(Execution.created_at.desc()).first()
|
||||
|
||||
if not execution:
|
||||
print("❌ 没有找到执行记录")
|
||||
return
|
||||
|
||||
print(f"\n✅ 找到执行记录: {execution.id}")
|
||||
print(f" 状态: {execution.status}")
|
||||
print(f" 执行时间: {execution.execution_time}ms")
|
||||
print(f" 创建时间: {execution.created_at}")
|
||||
|
||||
# 显示输入数据
|
||||
print("\n" + "=" * 80)
|
||||
print("输入数据 (input_data):")
|
||||
print("=" * 80)
|
||||
if execution.input_data:
|
||||
print(format_json(execution.input_data))
|
||||
else:
|
||||
print("(空)")
|
||||
|
||||
# 显示输出数据
|
||||
print("\n" + "=" * 80)
|
||||
print("输出数据 (output_data):")
|
||||
print("=" * 80)
|
||||
if execution.output_data:
|
||||
print(format_json(execution.output_data))
|
||||
else:
|
||||
print("(空)")
|
||||
|
||||
# 获取执行日志
|
||||
print("\n" + "=" * 80)
|
||||
print("执行日志 (按时间顺序):")
|
||||
print("=" * 80)
|
||||
|
||||
logs = db.query(ExecutionLog).filter(
|
||||
ExecutionLog.execution_id == execution.id
|
||||
).order_by(ExecutionLog.timestamp.asc()).all()
|
||||
|
||||
if not logs:
|
||||
print("❌ 没有找到执行日志")
|
||||
return
|
||||
|
||||
for i, log in enumerate(logs, 1):
|
||||
print(f"\n[{i}] {log.timestamp.strftime('%Y-%m-%d %H:%M:%S')} [{log.level}]")
|
||||
print(f" 节点: {log.node_id or '(无)'} ({log.node_type or '(无)'})")
|
||||
print(f" 消息: {log.message}")
|
||||
|
||||
if log.data:
|
||||
print(f" 数据:")
|
||||
data_str = format_json(log.data)
|
||||
# 只显示前500个字符
|
||||
if len(data_str) > 500:
|
||||
print(data_str[:500] + "...")
|
||||
else:
|
||||
print(data_str)
|
||||
|
||||
if log.duration:
|
||||
print(f" 耗时: {log.duration}ms")
|
||||
|
||||
# 特别关注LLM节点的输入输出
|
||||
print("\n" + "=" * 80)
|
||||
print("LLM节点详细分析:")
|
||||
print("=" * 80)
|
||||
|
||||
llm_logs = [log for log in logs if log.node_type == 'llm']
|
||||
if llm_logs:
|
||||
for log in llm_logs:
|
||||
if log.message == "节点开始执行" and log.data:
|
||||
print(f"\n节点 {log.node_id} 的输入数据:")
|
||||
input_data = log.data.get('input', {})
|
||||
print(format_json(input_data))
|
||||
|
||||
if log.message == "节点执行完成" and log.data:
|
||||
print(f"\n节点 {log.node_id} 的输出数据:")
|
||||
output_data = log.data.get('output', {})
|
||||
print(format_json(output_data))
|
||||
else:
|
||||
print("❌ 没有找到LLM节点的日志")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
445
frontend/src/components/WorkflowEditor/NodeExecutionDetail.vue
Normal file
445
frontend/src/components/WorkflowEditor/NodeExecutionDetail.vue
Normal file
@@ -0,0 +1,445 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
:title="`节点执行详情: ${nodeLabel}`"
|
||||
size="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-skeleton :rows="5" animated />
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="error-container">
|
||||
<el-alert
|
||||
type="error"
|
||||
:title="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="nodeLogs.length === 0" class="empty-container">
|
||||
<el-empty description="暂无执行日志" />
|
||||
</div>
|
||||
|
||||
<div v-else class="execution-detail">
|
||||
<!-- 节点基本信息 -->
|
||||
<el-card class="info-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>节点信息</span>
|
||||
<el-tag :type="statusType" size="small">{{ statusText }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="节点ID">{{ nodeId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="节点类型">{{ nodeType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="执行时间" v-if="executionTime">
|
||||
{{ executionTime }}ms
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行状态">
|
||||
<el-tag :type="statusType" size="small">{{ statusText }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 输入数据 -->
|
||||
<el-card v-if="inputData" class="data-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>输入数据</span>
|
||||
<el-button
|
||||
text
|
||||
size="small"
|
||||
@click="copyToClipboard(inputData)"
|
||||
title="复制"
|
||||
>
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="json-viewer">{{ formatJSON(inputData) }}</pre>
|
||||
</el-card>
|
||||
|
||||
<!-- 输出数据 -->
|
||||
<el-card v-if="outputData" class="data-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>输出数据</span>
|
||||
<el-button
|
||||
text
|
||||
size="small"
|
||||
@click="copyToClipboard(outputData)"
|
||||
title="复制"
|
||||
>
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="json-viewer">{{ formatJSON(outputData) }}</pre>
|
||||
</el-card>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<el-card v-if="errorInfo" class="error-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>错误信息</span>
|
||||
<el-tag type="danger" size="small">失败</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="error-content">
|
||||
<div v-if="errorInfo.error_type" class="error-type">
|
||||
<strong>错误类型:</strong>{{ errorInfo.error_type }}
|
||||
</div>
|
||||
<div class="error-message">
|
||||
<strong>错误消息:</strong>
|
||||
<pre>{{ errorInfo.error_message || errorInfo.error }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 执行时间线 -->
|
||||
<el-card class="timeline-card" shadow="never">
|
||||
<template #header>
|
||||
<span>执行时间线</span>
|
||||
</template>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(log, index) in nodeLogs"
|
||||
:key="index"
|
||||
:timestamp="formatTime(log.timestamp)"
|
||||
:type="getLogType(log.level)"
|
||||
:icon="getLogIcon(log.level)"
|
||||
>
|
||||
<div class="timeline-content">
|
||||
<div class="log-message">{{ log.message }}</div>
|
||||
<div v-if="log.duration" class="log-duration">
|
||||
耗时: {{ log.duration }}ms
|
||||
</div>
|
||||
<div v-if="log.data && Object.keys(log.data).length > 0" class="log-data">
|
||||
<el-collapse>
|
||||
<el-collapse-item title="查看详情" :name="index">
|
||||
<pre class="json-viewer-small">{{ formatJSON(log.data) }}</pre>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { DocumentCopy } from '@element-plus/icons-vue'
|
||||
import api from '@/api'
|
||||
|
||||
interface ExecutionLog {
|
||||
id: string
|
||||
node_id: string
|
||||
node_type: string
|
||||
level: string
|
||||
message: string
|
||||
data?: any
|
||||
timestamp: string
|
||||
duration?: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
nodeId?: string
|
||||
nodeLabel?: string
|
||||
nodeType?: string
|
||||
executionId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const nodeLogs = ref<ExecutionLog[]>([])
|
||||
|
||||
// 计算属性
|
||||
const visible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
})
|
||||
|
||||
const inputData = computed(() => {
|
||||
// 从日志中提取输入数据
|
||||
const startLog = nodeLogs.value.find(log =>
|
||||
log.message && (log.message.includes('开始执行') || log.message.includes('开始'))
|
||||
)
|
||||
if (startLog?.data?.input) {
|
||||
return startLog.data.input
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const outputData = computed(() => {
|
||||
// 从日志中提取输出数据
|
||||
const completeLog = nodeLogs.value.find(log =>
|
||||
log.message && (log.message.includes('执行完成') || log.message.includes('完成'))
|
||||
)
|
||||
if (completeLog?.data?.output) {
|
||||
return completeLog.data.output
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const errorInfo = computed(() => {
|
||||
// 从日志中提取错误信息
|
||||
const errorLog = nodeLogs.value.find(log =>
|
||||
log.level === 'ERROR' || (log.message && (log.message.includes('失败') || log.message.includes('错误')))
|
||||
)
|
||||
if (errorLog?.data) {
|
||||
return {
|
||||
error_type: errorLog.data.error_type,
|
||||
error_message: errorLog.data.error || errorLog.data.error_message || errorLog.message
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const executionTime = computed(() => {
|
||||
// 从日志中提取执行时间
|
||||
const completeLog = nodeLogs.value.find(log =>
|
||||
log.message && (log.message.includes('执行完成') || log.message.includes('完成'))
|
||||
)
|
||||
return completeLog?.duration || null
|
||||
})
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (errorInfo.value) return '失败'
|
||||
if (outputData.value) return '已完成'
|
||||
if (inputData.value) return '执行中'
|
||||
return '未知'
|
||||
})
|
||||
|
||||
const statusType = computed(() => {
|
||||
if (errorInfo.value) return 'danger'
|
||||
if (outputData.value) return 'success'
|
||||
if (inputData.value) return 'warning'
|
||||
return 'info'
|
||||
})
|
||||
|
||||
// 监听props变化,加载日志
|
||||
watch([() => props.visible, () => props.executionId, () => props.nodeId],
|
||||
([newVisible, newExecutionId, newNodeId]) => {
|
||||
if (newVisible && newExecutionId && newNodeId) {
|
||||
loadNodeLogs()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 加载节点日志
|
||||
const loadNodeLogs = async () => {
|
||||
if (!props.executionId || !props.nodeId) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
nodeLogs.value = []
|
||||
|
||||
try {
|
||||
const response = await api.get(`/api/v1/execution-logs/executions/${props.executionId}`, {
|
||||
params: {
|
||||
node_id: props.nodeId,
|
||||
limit: 100
|
||||
}
|
||||
})
|
||||
|
||||
nodeLogs.value = response.data || []
|
||||
|
||||
// 按时间排序
|
||||
nodeLogs.value.sort((a, b) => {
|
||||
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
})
|
||||
} catch (err: any) {
|
||||
console.error('加载节点日志失败:', err)
|
||||
const errorDetail = err.response?.data?.detail || err.message || '加载日志失败'
|
||||
error.value = errorDetail
|
||||
|
||||
// 如果是404错误,显示更友好的提示
|
||||
if (err.response?.status === 404) {
|
||||
ElMessage.warning('执行记录不存在或已过期,无法查看执行详情')
|
||||
} else {
|
||||
ElMessage.error('加载节点日志失败: ' + errorDetail)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化JSON
|
||||
const formatJSON = (data: any) => {
|
||||
if (!data) return ''
|
||||
try {
|
||||
if (typeof data === 'string') {
|
||||
// 尝试解析为JSON
|
||||
const parsed = JSON.parse(data)
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
}
|
||||
return JSON.stringify(data, null, 2)
|
||||
} catch {
|
||||
return String(data)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp: string) => {
|
||||
if (!timestamp) return ''
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
}
|
||||
|
||||
// 获取日志类型
|
||||
const getLogType = (level: string) => {
|
||||
switch (level) {
|
||||
case 'ERROR':
|
||||
return 'danger'
|
||||
case 'WARN':
|
||||
return 'warning'
|
||||
case 'INFO':
|
||||
return 'primary'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取日志图标
|
||||
const getLogIcon = (level: string) => {
|
||||
switch (level) {
|
||||
case 'ERROR':
|
||||
return 'CircleClose'
|
||||
case 'WARN':
|
||||
return 'Warning'
|
||||
case 'INFO':
|
||||
return 'CircleCheck'
|
||||
default:
|
||||
return 'InfoFilled'
|
||||
}
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
const copyToClipboard = async (data: any) => {
|
||||
try {
|
||||
const text = formatJSON(data)
|
||||
await navigator.clipboard.writeText(text)
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
} catch (err) {
|
||||
ElMessage.error('复制失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-container,
|
||||
.error-container,
|
||||
.empty-container {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.execution-detail {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.info-card,
|
||||
.data-card,
|
||||
.error-card,
|
||||
.timeline-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.json-viewer {
|
||||
background: #f5f7fa;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
overflow-x: auto;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.json-viewer-small {
|
||||
background: #f5f7fa;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.error-type {
|
||||
margin-bottom: 12px;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.error-message pre {
|
||||
background: #fef0f0;
|
||||
border: 1px solid #fde2e2;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.log-duration {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.log-data {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -114,9 +114,10 @@
|
||||
:pan-on-drag="true"
|
||||
:pan-on-scroll="true"
|
||||
:zoom-on-scroll="true"
|
||||
:zoom-on-double-click="true"
|
||||
:zoom-on-double-click="false"
|
||||
:fit-view-on-init="false"
|
||||
@node-click="onNodeClick"
|
||||
@node-double-click="onNodeDoubleClick"
|
||||
@edge-click="onEdgeClick"
|
||||
@pane-click="onPaneClick"
|
||||
@nodes-change="onNodesChange"
|
||||
@@ -1233,6 +1234,15 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 节点执行详情面板 -->
|
||||
<NodeExecutionDetail
|
||||
v-model:visible="nodeDetailVisible"
|
||||
:node-id="selectedNode?.id"
|
||||
:node-label="selectedNode?.data?.label || selectedNode?.id"
|
||||
:node-type="selectedNode?.type"
|
||||
:execution-id="currentExecutionId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1251,6 +1261,7 @@ import api from '@/api'
|
||||
import type { WorkflowNode, WorkflowEdge } from '@/types'
|
||||
import { StartNode, LLMNode, ConditionNode, EndNode, DefaultNode } from './NodeTypes'
|
||||
import { useCollaboration } from '@/composables/useCollaboration'
|
||||
import NodeExecutionDetail from './NodeExecutionDetail.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
workflowId?: string
|
||||
@@ -1271,6 +1282,8 @@ const selectedNode = ref<Node | null>(null)
|
||||
const selectedEdge = ref<Edge | null>(null)
|
||||
const draggedNodeType = ref<any>(null)
|
||||
const testingNode = ref(false)
|
||||
const nodeDetailVisible = ref(false)
|
||||
const currentExecutionId = ref<string | null>(null)
|
||||
|
||||
// 节点复制相关
|
||||
const copiedNode = ref<Node | null>(null)
|
||||
@@ -1640,6 +1653,20 @@ const onNodeClick = (event: NodeClickEvent) => {
|
||||
nodeTestResult.value = null
|
||||
}
|
||||
|
||||
// 节点双击 - 显示执行详情
|
||||
const onNodeDoubleClick = (event: NodeClickEvent) => {
|
||||
// 阻止双击时的默认行为(如缩放)
|
||||
event.event?.preventDefault?.()
|
||||
|
||||
if (currentExecutionId.value) {
|
||||
// 确保选中节点
|
||||
selectedNode.value = event.node
|
||||
nodeDetailVisible.value = true
|
||||
} else {
|
||||
ElMessage.info('暂无执行记录,请先执行工作流')
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为定时任务节点类型(计算属性)
|
||||
const isScheduleNodeSelected = computed(() => {
|
||||
return selectedNode.value && (selectedNode.value.type === 'schedule' || selectedNode.value.type === 'delay' || selectedNode.value.type === 'timer')
|
||||
@@ -2699,6 +2726,11 @@ watch(() => props.executionStatus, (newStatus, oldStatus) => {
|
||||
console.log('[rjb] Current nodes:', nodes.value.map(n => ({ id: n.id, type: n.type, class: n.class })))
|
||||
console.log('[rjb] All node IDs:', nodes.value.map(n => n.id))
|
||||
|
||||
// 提取execution_id
|
||||
if (newStatus && newStatus.execution_id) {
|
||||
currentExecutionId.value = newStatus.execution_id
|
||||
}
|
||||
|
||||
// 使用 nextTick 确保 DOM 更新
|
||||
nextTick(() => {
|
||||
// 清除所有节点的执行状态
|
||||
|
||||
121
test_node_input_output.md
Normal file
121
test_node_input_output.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# 工作流数据流转测试方案
|
||||
|
||||
## 问题描述
|
||||
工作流能执行完成,但LLM节点输出"答非所问",可能是数据在节点间传递时被错误处理。
|
||||
|
||||
## 测试步骤
|
||||
|
||||
### 方法1: 使用测试脚本(推荐)
|
||||
|
||||
1. **运行测试脚本**:
|
||||
```bash
|
||||
cd /home/renjianbo/aiagent
|
||||
python3 test_workflow_data_flow.py
|
||||
```
|
||||
|
||||
2. **观察输出**:
|
||||
- 查看每个节点的输入数据
|
||||
- 特别关注LLM节点的输入数据格式
|
||||
- 检查是否有嵌套的`{"input": {...}}`结构
|
||||
|
||||
### 方法2: 查看执行日志数据库
|
||||
|
||||
1. **查询最近的执行记录**:
|
||||
```sql
|
||||
SELECT id, input_data, output_data, status, execution_time
|
||||
FROM executions
|
||||
WHERE agent_id IS NOT NULL
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1;
|
||||
```
|
||||
|
||||
2. **查询节点执行日志**:
|
||||
```sql
|
||||
SELECT node_id, node_type, level, message, data, timestamp
|
||||
FROM execution_logs
|
||||
WHERE execution_id = 'YOUR_EXECUTION_ID'
|
||||
ORDER BY timestamp ASC;
|
||||
```
|
||||
|
||||
### 方法3: 添加临时调试代码
|
||||
|
||||
在 `backend/app/services/workflow_engine.py` 的 `get_node_input` 方法中添加:
|
||||
|
||||
```python
|
||||
# 在方法开始处
|
||||
if node_id == 'llm-1': # 你的LLM节点ID
|
||||
import json
|
||||
print(f"[DEBUG] get_node_input for {node_id}")
|
||||
print(f"[DEBUG] node_outputs: {json.dumps(node_outputs, ensure_ascii=False, indent=2)}")
|
||||
print(f"[DEBUG] source_output: {json.dumps(node_outputs.get('start-1', {}), ensure_ascii=False, indent=2)}")
|
||||
```
|
||||
|
||||
在 `execute_node` 方法的LLM节点处理部分添加:
|
||||
|
||||
```python
|
||||
# 在格式化prompt之前
|
||||
if node_type == 'llm':
|
||||
import json
|
||||
print(f"[DEBUG] LLM节点输入: {json.dumps(input_data, ensure_ascii=False, indent=2)}")
|
||||
print(f"[DEBUG] 原始prompt: {prompt}")
|
||||
print(f"[DEBUG] user_query提取结果: {user_query}")
|
||||
print(f"[DEBUG] 最终formatted_prompt: {formatted_prompt}")
|
||||
```
|
||||
|
||||
然后重启后端并测试:
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml restart backend
|
||||
```
|
||||
|
||||
### 方法4: 使用浏览器开发者工具
|
||||
|
||||
1. 打开浏览器开发者工具(F12)
|
||||
2. 切换到 Network 标签
|
||||
3. 执行一次测试
|
||||
4. 查看请求:
|
||||
- `POST /api/v1/executions` - 查看发送的 `input_data`
|
||||
5. 查看响应:
|
||||
- `GET /api/v1/executions/{id}/status` - 查看执行状态
|
||||
- `GET /api/v1/executions/{id}` - 查看执行结果
|
||||
|
||||
## 关键检查点
|
||||
|
||||
### 1. Start节点输出
|
||||
- **期望**: `{"query": "苹果英语怎么讲?", "USER_INPUT": "苹果英语怎么讲?"}`
|
||||
- **实际**: 检查是否被包装成 `{"input": {...}}` 或其他格式
|
||||
|
||||
### 2. LLM节点输入
|
||||
- **期望**: 直接从start节点获取,格式为 `{"query": "...", "USER_INPUT": "..."}`
|
||||
- **实际**: 检查 `get_node_input` 返回的数据格式
|
||||
|
||||
### 3. user_query提取
|
||||
- **期望**: 能正确提取到 "苹果英语怎么讲?"
|
||||
- **实际**: 检查提取逻辑是否正确处理嵌套结构
|
||||
|
||||
### 4. Prompt格式化
|
||||
- **期望**: 如果是通用指令"请处理用户请求。",应该直接使用user_query作为prompt
|
||||
- **实际**: 检查最终的formatted_prompt内容
|
||||
|
||||
## 预期问题位置
|
||||
|
||||
根据图片分析,输入数据可能是:
|
||||
```json
|
||||
{
|
||||
"input": {
|
||||
"query": "苹果英语怎么讲?",
|
||||
"USER_INPUT": "苹果英语怎么讲?"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这可能是以下原因导致的:
|
||||
1. `get_node_input` 方法在某个分支将数据包装成了 `{"input": ...}`
|
||||
2. `start` 节点的输出被错误保存
|
||||
3. 数据在节点间传递时被错误处理
|
||||
|
||||
## 修复建议
|
||||
|
||||
如果发现数据被包装成 `{"input": {...}}`,需要:
|
||||
1. 在 `get_node_input` 中正确处理嵌套的 `input` 字段
|
||||
2. 确保 `user_query` 提取逻辑能处理这种嵌套结构
|
||||
3. 确保prompt格式化逻辑能正确使用提取的 `user_query`
|
||||
169
test_workflow_data_flow.py
Executable file
169
test_workflow_data_flow.py
Executable file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
工作流数据流转测试脚本
|
||||
用于诊断"答非所问"问题
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||
|
||||
from app.services.workflow_engine import WorkflowEngine
|
||||
from app.core.database import SessionLocal
|
||||
|
||||
|
||||
def print_section(title):
|
||||
"""打印分隔线"""
|
||||
print("\n" + "=" * 80)
|
||||
print(f" {title}")
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
def print_data(label, data, indent=0):
|
||||
"""格式化打印数据"""
|
||||
prefix = " " * indent
|
||||
print(f"{prefix}{label}:")
|
||||
if isinstance(data, dict):
|
||||
print(f"{prefix} {json.dumps(data, ensure_ascii=False, indent=2)}")
|
||||
else:
|
||||
print(f"{prefix} {data}")
|
||||
|
||||
|
||||
async def test_workflow_data_flow():
|
||||
"""测试工作流数据流转"""
|
||||
print_section("工作流数据流转测试")
|
||||
|
||||
# 模拟一个简单的工作流
|
||||
workflow_data = {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start-1",
|
||||
"type": "start",
|
||||
"position": {"x": 100, "y": 100},
|
||||
"data": {
|
||||
"label": "开始"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "llm-1",
|
||||
"type": "llm",
|
||||
"position": {"x": 300, "y": 100},
|
||||
"data": {
|
||||
"label": "LLM处理",
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"prompt": "请处理用户请求。",
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 1500
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "end-1",
|
||||
"type": "end",
|
||||
"position": {"x": 500, "y": 100},
|
||||
"data": {
|
||||
"label": "结束",
|
||||
"output_format": "text"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{"id": "e1", "source": "start-1", "target": "llm-1"},
|
||||
{"id": "e2", "source": "llm-1", "target": "end-1"}
|
||||
]
|
||||
}
|
||||
|
||||
# 模拟前端发送的输入数据
|
||||
input_data = {
|
||||
"query": "苹果英语怎么讲?",
|
||||
"USER_INPUT": "苹果英语怎么讲?"
|
||||
}
|
||||
|
||||
print_section("1. 初始输入数据")
|
||||
print_data("input_data", input_data)
|
||||
|
||||
# 创建引擎(不使用logger,避免数据库依赖)
|
||||
engine = WorkflowEngine("test-workflow", workflow_data)
|
||||
|
||||
# 重写get_node_input方法,添加详细日志
|
||||
original_get_node_input = engine.get_node_input
|
||||
|
||||
def logged_get_node_input(node_id, node_outputs, active_edges=None):
|
||||
print_section(f"获取节点输入: {node_id}")
|
||||
print_data("node_outputs", node_outputs)
|
||||
result = original_get_node_input(node_id, node_outputs, active_edges)
|
||||
print_data(f"返回的input_data (for {node_id})", result)
|
||||
return result
|
||||
|
||||
engine.get_node_input = logged_get_node_input
|
||||
|
||||
# 重写execute_node方法,添加详细日志
|
||||
original_execute_node = engine.execute_node
|
||||
|
||||
async def logged_execute_node(node, input_data):
|
||||
node_id = node.get('id')
|
||||
node_type = node.get('type')
|
||||
|
||||
print_section(f"执行节点: {node_id} ({node_type})")
|
||||
print_data("节点配置", node.get('data', {}))
|
||||
print_data("输入数据", input_data)
|
||||
|
||||
result = await original_execute_node(node, input_data)
|
||||
|
||||
print_data("执行结果", result)
|
||||
|
||||
# 如果是LLM节点,特别关注prompt和输出
|
||||
if node_type == 'llm':
|
||||
print_section(f"LLM节点详细分析: {node_id}")
|
||||
node_data = node.get('data', {})
|
||||
prompt = node_data.get('prompt', '')
|
||||
print_data("原始prompt", prompt)
|
||||
print_data("输入数据", input_data)
|
||||
|
||||
# 模拟prompt格式化逻辑
|
||||
if isinstance(input_data, dict):
|
||||
# 检查是否有嵌套的input字段
|
||||
nested_input = input_data.get('input')
|
||||
if isinstance(nested_input, dict):
|
||||
print("⚠️ 发现嵌套的input字段!")
|
||||
print_data("嵌套input内容", nested_input)
|
||||
# 尝试提取user_query
|
||||
user_query = None
|
||||
for key in ['query', 'input', 'text', 'message', 'content', 'user_input', 'USER_INPUT']:
|
||||
if key in nested_input:
|
||||
user_query = nested_input[key]
|
||||
print(f"✅ 从嵌套input中提取到user_query: {key} = {user_query}")
|
||||
break
|
||||
else:
|
||||
# 从顶层提取
|
||||
user_query = None
|
||||
for key in ['query', 'input', 'text', 'message', 'content', 'user_input', 'USER_INPUT']:
|
||||
if key in input_data:
|
||||
value = input_data[key]
|
||||
if isinstance(value, str):
|
||||
user_query = value
|
||||
print(f"✅ 从顶层提取到user_query: {key} = {user_query}")
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
engine.execute_node = logged_execute_node
|
||||
|
||||
# 执行工作流
|
||||
print_section("开始执行工作流")
|
||||
try:
|
||||
result = await engine.execute(input_data)
|
||||
print_section("工作流执行完成")
|
||||
print_data("最终结果", result)
|
||||
except Exception as e:
|
||||
print_section("执行出错")
|
||||
print(f"错误: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_workflow_data_flow())
|
||||
8
view_logs.sh
Executable file
8
view_logs.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# 查看工作流调试日志的脚本
|
||||
|
||||
echo "正在查看后端日志(包含 [rjb] 调试信息)..."
|
||||
echo "按 Ctrl+C 退出"
|
||||
echo ""
|
||||
|
||||
docker-compose -f docker-compose.dev.yml logs -f backend | grep --line-buffered "\[rjb\]"
|
||||
88
测试方案总结.md
Normal file
88
测试方案总结.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 工作流数据流转测试方案总结
|
||||
|
||||
## 问题
|
||||
工作流能执行完成,但LLM节点输出"答非所问",可能是数据在节点间传递时被错误处理。
|
||||
|
||||
## 快速测试方法
|
||||
|
||||
### 方法1: 查看执行日志数据库(最简单)
|
||||
|
||||
```bash
|
||||
cd /home/renjianbo/aiagent
|
||||
python3 check_execution_logs.py
|
||||
```
|
||||
|
||||
这个脚本会:
|
||||
- 自动查找最近的Agent执行记录
|
||||
- 显示输入数据和输出数据
|
||||
- 显示所有执行日志(按时间顺序)
|
||||
- 特别分析LLM节点的输入输出
|
||||
|
||||
### 方法2: 运行数据流转测试脚本
|
||||
|
||||
```bash
|
||||
cd /home/renjianbo/aiagent
|
||||
python3 test_workflow_data_flow.py
|
||||
```
|
||||
|
||||
这个脚本会:
|
||||
- 模拟完整的工作流执行
|
||||
- 详细记录每个节点的输入输出
|
||||
- 特别关注数据格式转换
|
||||
|
||||
### 方法3: 查看后端日志
|
||||
|
||||
```bash
|
||||
# 查看包含[rjb]的调试日志
|
||||
docker-compose -f docker-compose.dev.yml logs --tail=500 backend | grep "\[rjb\]" | tail -50
|
||||
|
||||
# 或者查看Celery worker的日志
|
||||
docker-compose -f docker-compose.dev.yml logs --tail=500 celery | grep "\[rjb\]" | tail -50
|
||||
```
|
||||
|
||||
## 关键检查点
|
||||
|
||||
### 1. 输入数据格式
|
||||
**期望**: `{"query": "苹果英语怎么讲?", "USER_INPUT": "苹果英语怎么讲?"}`
|
||||
|
||||
**检查**: 在浏览器开发者工具的Network标签中查看 `POST /api/v1/executions` 请求的body
|
||||
|
||||
### 2. Start节点输出
|
||||
**期望**: 直接返回输入数据,格式不变
|
||||
|
||||
**检查**: 查看执行日志中start节点的输出
|
||||
|
||||
### 3. LLM节点输入
|
||||
**期望**: 从start节点获取,格式为 `{"query": "...", "USER_INPUT": "..."}`
|
||||
|
||||
**可能的问题**: 被包装成 `{"input": {"query": "...", "USER_INPUT": "..."}}`
|
||||
|
||||
**检查**:
|
||||
- 查看执行日志中LLM节点开始执行时的输入数据
|
||||
- 或者运行 `check_execution_logs.py` 查看详细日志
|
||||
|
||||
### 4. user_query提取
|
||||
**期望**: 能正确提取到 "苹果英语怎么讲?"
|
||||
|
||||
**检查**: 查看后端日志中的 `[rjb] 最终提取的user_query` 日志
|
||||
|
||||
### 5. Prompt格式化
|
||||
**期望**: 如果是通用指令"请处理用户请求。",应该直接使用user_query作为prompt
|
||||
|
||||
**检查**: 查看后端日志中的prompt相关日志
|
||||
|
||||
## 预期问题
|
||||
|
||||
根据之前的分析,最可能的问题是:
|
||||
|
||||
1. **数据被包装**: `get_node_input` 方法可能将数据包装成了 `{"input": {...}}`
|
||||
2. **提取逻辑问题**: `user_query` 提取逻辑可能没有正确处理嵌套结构
|
||||
3. **Prompt格式化问题**: 即使提取到了 `user_query`,prompt格式化可能没有正确使用
|
||||
|
||||
## 修复建议
|
||||
|
||||
如果发现数据被包装成 `{"input": {...}}`,我已经在代码中添加了处理逻辑:
|
||||
- 在 `get_node_input` 中检查嵌套的 `input` 字段
|
||||
- 在 `user_query` 提取时优先从嵌套的 `input` 中提取
|
||||
|
||||
如果问题仍然存在,请运行测试脚本并提供输出结果。
|
||||
Reference in New Issue
Block a user