feat: 更新工作流引擎和数据库日志记录逻辑

- 在工作流执行过程中增强异常处理,使用logger.exception记录详细的错误信息和堆栈信息,便于调试
- 更新DrSession类的desc方法,添加更详细的日志记录,包含调用位置
- 在LLM节点执行中添加流式输出支持,改进提示词构建逻辑,确保更准确的用户查询响应
- 更新数据库文件和二进制数据,确保数据一致性
This commit is contained in:
eason 2026-01-28 20:14:29 +08:00
parent 643c2f90c4
commit e308e9d2f2
5 changed files with 366 additions and 162 deletions

Binary file not shown.

View File

@ -424,7 +424,12 @@ async def execute_workflow_stream(
yield f"data: {json.dumps({'type': 'workflow_complete', 'message': '工作流执行完成', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" yield f"data: {json.dumps({'type': 'workflow_complete', 'message': '工作流执行完成', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n"
except Exception as e: except Exception as e:
logger.error(f"流式工作流执行异常: {e}", exc_info=True) # 这里捕获到的通常是内部节点或引擎抛出的异常,比如 KeyError("'pk_1'")
# 使用 exception 打印完整堆栈,并记录异常类型与 repr方便排查
logger.exception(
f"流式工作流执行异常type={type(e).__name__}, repr={repr(e)}"
)
# 将错误信息推送给前端
yield f"data: {json.dumps({'type': 'error', 'message': f'工作流执行失败: {str(e)}'}, ensure_ascii=False)}\n\n" yield f"data: {json.dumps({'type': 'error', 'message': f'工作流执行失败: {str(e)}'}, ensure_ascii=False)}\n\n"
response = StreamingResponse( response = StreamingResponse(

View File

@ -50,7 +50,14 @@ class DrSession(AsyncSession):
def desc(self, value: str) -> None: def desc(self, value: str) -> None:
"""Set work brief in session info.""" """Set work brief in session info."""
self.stepIndex += 1 self.stepIndex += 1
logger.info(value) # 统一在这里打印更详细的 session 日志,方便排查问题
try:
# level 取 -3可以拿到触发 desc 设置的上层业务代码位置
pos = self.parse_source_pos(-3)
except Exception:
pos = "unknown"
logger.info(f"{self.log_prefix()} STEP[{self.stepIndex}] {value} >>> @ {pos}")
def log_prefix(self) -> str: def log_prefix(self) -> str:
"""Get log prefix with session ID and desc.""" """Get log prefix with session ID and desc."""
@ -118,7 +125,10 @@ async def get_session(request: Request = None):
client_host = request.client.host client_host = request.client.host
else: else:
client_host = "无request" client_host = "无request"
session = DrSession(bind=engine_async)
# 使用 AsyncSessionFactory 创建会话,确保 async/greenlet 配置正确
#(包括 expire_on_commit=False避免在属性访问时触发隐式 IO导致 MissingGreenlet / pk_1 参数异常)
session: DrSession = AsyncSessionFactory()
session.title = f"{url} - {client_host}" session.title = f"{url} - {client_host}"
@ -131,6 +141,9 @@ async def get_session(request: Request = None):
except Exception as e: except Exception as e:
errMsg = f"数据库 session 异常 >>> {e}" errMsg = f"数据库 session 异常 >>> {e}"
# 先打带堆栈的异常日志
session.log_exception(errMsg)
# 再通过 desc 打一条结构化的 info 日志(含步骤、调用位置)
session.desc = f"EXCEPTION: {errMsg}" session.desc = f"EXCEPTION: {errMsg}"
await session.rollback() await session.rollback()
# 重新抛出原始异常,不转换为 HTTPException # 重新抛出原始异常,不转换为 HTTPException

View File

@ -177,7 +177,8 @@ class WorkflowEngine:
} }
except Exception as e: except Exception as e:
logger.error(f"工作流执行失败: {str(e)}") # 打印完整堆栈,方便排查如 KeyError("'pk_1'") 之类的问题
logger.exception(f"工作流执行失败: {str(e)}")
execution.status = ExecutionStatus.FAILED execution.status = ExecutionStatus.FAILED
execution.error_message = str(e) execution.error_message = str(e)
execution.completed_at = datetime.now().isoformat() execution.completed_at = datetime.now().isoformat()
@ -306,6 +307,7 @@ class WorkflowEngine:
node_info = node_graph[node_id] node_info = node_graph[node_id]
node = node_info['node'] node = node_info['node']
node_type = node.get('type', '')
# 等待所有输入节点完成 # 等待所有输入节点完成
for input_node_id in node_info['inputs']: for input_node_id in node_info['inputs']:
@ -328,7 +330,36 @@ class WorkflowEngine:
try: try:
# 执行当前节点 # 执行当前节点
if node_type == 'llm':
# 对 LLM 节点使用真正的流式执行
output = None
async for event in self._execute_llm_node_stream(execution, node, context):
# event 统一为内部事件,包含 event_type 字段
if event.get('event_type') == 'delta':
# 向前端推送流式增量输出
yield {
'type': 'node_stream',
'execution_id': execution.id,
'node_id': node_id,
'status': 'streaming',
'data': {
'node_name': node.get('name', ''),
'node_type': node_type,
'delta': event.get('delta', ''),
'full_response': event.get('full_response', '')
},
'timestamp': datetime.now().isoformat()
}
elif event.get('event_type') == 'final':
# 最终完整输出,供后续节点使用
output = event.get('output', {})
if output is None:
output = {}
else:
# 非 LLM 节点仍然走原来的单次执行逻辑
output = await self._execute_single_node(execution, node, context) output = await self._execute_single_node(execution, node, context)
context['node_outputs'][node_id] = output context['node_outputs'][node_id] = output
# 发送节点完成的消息 # 发送节点完成的消息
@ -416,6 +447,12 @@ class WorkflowEngine:
try: try:
# 准备输入数据 # 准备输入数据
input_data = self._prepare_node_input(node, context) input_data = self._prepare_node_input(node, context)
# 这里打印节点级别的输入数据,辅助定位 KeyError 等问题
try:
logger.info(f"执行节点 {node_id} ({node_type}) 输入数据: {json.dumps(input_data, ensure_ascii=False)[:2000]}")
except Exception:
# 有些数据不可序列化,退化为直接打印 repr
logger.info(f"执行节点 {node_id} ({node_type}) 输入数据(非JSON): {repr(input_data)[:2000]}")
# 为前端显示准备输入数据 # 为前端显示准备输入数据
display_input_data = input_data.copy() display_input_data = input_data.copy()
@ -476,7 +513,11 @@ class WorkflowEngine:
return output_data return output_data
except Exception as e: except Exception as e:
logger.error(f"节点 {node_id} 执行失败: {str(e)}") # 记录更详细的节点异常信息(包含堆栈)
logger.exception(
f"节点执行失败 - id={node_id}, type={node_type}, name={node_name}, "
f"error_type={type(e).__name__}, error={str(e)}"
)
end_time = time.time() end_time = time.time()
node_execution.status = ExecutionStatus.FAILED node_execution.status = ExecutionStatus.FAILED
node_execution.error_message = str(e) node_execution.error_message = str(e)
@ -650,83 +691,18 @@ class WorkflowEngine:
'data': result_data 'data': result_data
} }
async def _execute_llm_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: def _build_llm_prompt(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> (str, str):
"""执行LLM节点""" """
根据节点配置知识库结果和工作流输入构建提示词
返回 (prompt, prompt_template)
- prompt: 变量替换后的最终提示词
- prompt_template: 原始模板未替换变量
"""
config = input_data.get('node_config', {}) config = input_data.get('node_config', {})
# 标记是否已经使用了默认模型(用于错误时决定是否回退)
used_default_model = False
# 获取LLM配置
model_id = config.get('model_id')
if not model_id:
# 兼容前端的model字段可能是ID或名称
model_value = config.get('model_name', config.get('model'))
if model_value:
# 如果是整数直接作为ID使用
if isinstance(model_value, int):
model_id = model_value
else:
# 如果是字符串,按名称查询
from sqlalchemy import select
result = await self.session.execute(
select(LLMConfig).where(LLMConfig.model_name == model_value)
)
llm_cfg = result.scalar_one_or_none()
if llm_cfg:
model_id = llm_cfg.id
# 如果还是没有,尝试从节点定义本身获取
if not model_id:
node_config = node.get('config', {})
model_id = node_config.get('model_id')
if not model_id:
model_value = node_config.get('model_name', node_config.get('model'))
if model_value:
if isinstance(model_value, int):
model_id = model_value
else:
from sqlalchemy import select
result = await self.session.execute(
select(LLMConfig).where(LLMConfig.model_name == model_value)
)
llm_cfg = result.scalar_one_or_none()
if llm_cfg:
model_id = llm_cfg.id
# 如果还是没有尝试使用默认的LLM配置
if not model_id:
from ..services.llm_config_service import LLMConfigService
llm_config_service = LLMConfigService()
default_config = await llm_config_service.get_default_chat_config(self.session)
if default_config:
model_id = default_config.id
used_default_model = True
logger.info(f"LLM节点未指定模型配置使用默认模型: {default_config.model_name} (ID: {model_id})")
else:
raise ValueError(
"未指定有效的大模型配置,且未找到默认配置。\n"
"请在节点配置中添加模型ID或模型名称例如\n"
" - config.model_id: 1\n"
" - config.model_name: 'gpt-4'\n"
" - config.model: 'gpt-4'\n"
"或者设置一个默认的LLM配置。"
)
from sqlalchemy import select
result = await self.session.execute(
select(LLMConfig).where(LLMConfig.id == model_id)
)
llm_config = result.scalar_one_or_none()
if not llm_config:
raise ValueError(f"大模型配置 {model_id} 不存在")
# 准备提示词
prompt_template = config.get('prompt', '') prompt_template = config.get('prompt', '')
# 如果提示词为空,尝试自动构建提示词 # 如果提示词为空尝试自动构建提示词RAG 或直接用用户输入)
if not prompt_template: if not prompt_template:
# 检查是否有知识库搜索结果
previous_outputs = input_data.get('previous_outputs', {}) previous_outputs = input_data.get('previous_outputs', {})
knowledge_base_results = None knowledge_base_results = None
user_query = None user_query = None
@ -741,7 +717,6 @@ class WorkflowEngine:
# 如果没有找到知识库结果,尝试从工作流输入中获取查询 # 如果没有找到知识库结果,尝试从工作流输入中获取查询
if not user_query: if not user_query:
workflow_input = input_data.get('workflow_input', {}) workflow_input = input_data.get('workflow_input', {})
# 尝试获取第一个非空字符串值作为查询
for key, value in workflow_input.items(): for key, value in workflow_input.items():
if isinstance(value, str) and value.strip(): if isinstance(value, str) and value.strip():
user_query = value.strip() user_query = value.strip()
@ -749,23 +724,20 @@ class WorkflowEngine:
# 构建提示词 # 构建提示词
if knowledge_base_results and len(knowledge_base_results) > 0: if knowledge_base_results and len(knowledge_base_results) > 0:
# 检查知识库结果的相似度,判断是否相关
max_score = 0 max_score = 0
for result in knowledge_base_results: for result in knowledge_base_results:
score = result.get('normalized_score', result.get('similarity_score', 0)) score = result.get('normalized_score', result.get('similarity_score', 0))
if score > max_score: if score > max_score:
max_score = score max_score = score
# 如果最高相似度分数很低低于0.5),认为结果不相关
is_relevant = max_score >= 0.5 is_relevant = max_score >= 0.5
if is_relevant: if is_relevant:
# 有相关的知识库结果,构建 RAG 风格的提示词 # 有相关的知识库结果,构建 RAG 风格的提示词
context_parts = [] context_parts = []
for i, result in enumerate(knowledge_base_results[:5], 1): # 取前5个结果 for i, result in enumerate(knowledge_base_results[:5], 1):
content = result.get('content', '').strip() content = result.get('content', '').strip()
if content: if content:
# 限制每个结果的长度,避免提示词过长
max_length = 1000 max_length = 1000
if len(content) > max_length: if len(content) > max_length:
content = content[:max_length] + "..." content = content[:max_length] + "..."
@ -785,40 +757,117 @@ class WorkflowEngine:
- 即使文档没有直接定义也要基于文档中的相关内容进行解释和说明 - 即使文档没有直接定义也要基于文档中的相关内容进行解释和说明
- 如果文档中提到了相关概念政策法规等请基于这些内容进行回答 - 如果文档中提到了相关概念政策法规等请基于这些内容进行回答
- 回答要准确详细有条理尽量引用文档中的具体内容""" - 回答要准确详细有条理尽量引用文档中的具体内容"""
logger.info(f"自动构建RAG提示词包含 {len(knowledge_base_results)} 个相关知识库结果(最高相似度: {max_score:.3f}),用户问题: {user_query}") logger.info(
f"自动构建RAG提示词包含 {len(knowledge_base_results)} 个相关知识库结果(最高相似度: {max_score:.3f}),用户问题: {user_query}"
)
else: else:
# 知识库结果不相关,直接回答用户问题 logger.warning(
logger.warning(f"知识库结果相似度较低(最高: {max_score:.3f}),认为不相关,将直接回答用户问题") f"知识库结果相似度较低(最高: {max_score:.3f}),认为不相关,将直接回答用户问题"
)
prompt_template = user_query or "请帮助我处理这个任务。" prompt_template = user_query or "请帮助我处理这个任务。"
elif user_query: elif user_query:
# 没有知识库结果,但有用户查询,构建简单提示词
prompt_template = user_query prompt_template = user_query
logger.info(f"自动使用工作流输入作为提示词: {user_query}") logger.info(f"自动使用工作流输入作为提示词: {user_query}")
else: else:
# 既没有知识库结果,也没有用户查询
prompt_template = "请帮助我处理这个任务。" prompt_template = "请帮助我处理这个任务。"
logger.warning("LLM节点提示词为空且无法从上下文获取使用默认提示词") logger.warning("LLM节点提示词为空且无法从上下文获取使用默认提示词")
# 检查是否启用变量替换 # 变量替换
enable_variable_substitution = config.get('enable_variable_substitution', True) enable_variable_substitution = config.get('enable_variable_substitution', True)
if enable_variable_substitution: if enable_variable_substitution:
# 使用增强的变量替换
prompt = self._substitute_variables(prompt_template, input_data) prompt = self._substitute_variables(prompt_template, input_data)
else: else:
prompt = prompt_template prompt = prompt_template
return prompt, prompt_template
async def _execute_llm_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]:
"""执行LLM节点非流式"""
config = input_data.get('node_config', {})
# 标记是否已经使用了默认模型(用于错误时决定是否回退)
used_default_model = False
# 获取 LLM 配置
model_id = config.get('model_id')
if not model_id:
model_value = config.get('model_name', config.get('model'))
if model_value:
if isinstance(model_value, int):
model_id = model_value
else:
from sqlalchemy import select
result = await self.session.execute(
select(LLMConfig).where(LLMConfig.model_name == model_value)
)
llm_cfg = result.scalar_one_or_none()
if llm_cfg:
model_id = llm_cfg.id
if not model_id:
node_config = node.get('config', {})
model_id = node_config.get('model_id')
if not model_id:
model_value = node_config.get('model_name', node_config.get('model'))
if model_value:
if isinstance(model_value, int):
model_id = model_value
else:
from sqlalchemy import select
result = await self.session.execute(
select(LLMConfig).where(LLMConfig.model_name == model_value)
)
llm_cfg = result.scalar_one_or_none()
if llm_cfg:
model_id = llm_cfg.id
if not model_id:
from ..services.llm_config_service import LLMConfigService
llm_config_service = LLMConfigService()
default_config = await llm_config_service.get_default_chat_config(self.session)
if default_config:
model_id = default_config.id
used_default_model = True
logger.info(
f"LLM节点未指定模型配置使用默认模型: {default_config.model_name} (ID: {model_id})"
)
else:
raise ValueError(
"未指定有效的大模型配置,且未找到默认配置。\n"
"请在节点配置中添加模型ID或模型名称例如\n"
" - config.model_id: 1\n"
" - config.model_name: 'gpt-4'\n"
" - config.model: 'gpt-4'\n"
"或者设置一个默认的LLM配置。"
)
from sqlalchemy import select
result = await self.session.execute(
select(LLMConfig).where(LLMConfig.id == model_id)
)
llm_config = result.scalar_one_or_none()
if not llm_config:
raise ValueError(f"大模型配置 {model_id} 不存在")
# 使用统一的构建逻辑生成提示词
prompt, prompt_template = self._build_llm_prompt(node, input_data)
logger.info(
f"LLM 节点最终提示词(非流式): node_id={node.get('id')}, "
f"model_id={llm_config.id}, model_name={llm_config.model_name}, prompt={prompt}"
)
# 记录处理后的提示词到输入数据中,用于前端显示 # 记录处理后的提示词到输入数据中,用于前端显示
input_data['processed_prompt'] = prompt input_data['processed_prompt'] = prompt
input_data['original_prompt'] = prompt_template input_data['original_prompt'] = prompt_template
# 调用LLM服务 # 调用 LLM 服务(非流式路径:用于 /execute 接口)
try: try:
response = await self.llm_service.chat_completion( response = await self.llm_service.chat_completion(
model_config=llm_config, model_config=llm_config,
messages=[{"role": "user", "content": prompt}], messages=[{"role": "user", "content": prompt}],
temperature=config.get('temperature', 0.7), temperature=config.get('temperature', 0.7),
max_tokens=config.get('max_tokens') max_tokens=config.get('max_tokens'),
) )
return { return {
@ -826,61 +875,198 @@ class WorkflowEngine:
'response': response, 'response': response,
'prompt': prompt, 'prompt': prompt,
'model': llm_config.model_name, 'model': llm_config.model_name,
'tokens_used': getattr(response, 'usage', {}).get('total_tokens', 0) if hasattr(response, 'usage') else 0 'tokens_used': getattr(response, 'usage', {}).get('total_tokens', 0)
if hasattr(response, 'usage')
else 0,
} }
except Exception as e: except Exception as e:
error_msg = str(e) error_msg = str(e)
# LLMService 已经添加了详细的错误信息包括处理后的模型名称和base_url直接使用
# 如果错误信息中已经包含了模型信息,就不再重复添加
detailed_error = error_msg detailed_error = error_msg
# 如果错误信息中还没有包含模型配置信息,则添加(使用原始配置作为补充)
if "使用的模型:" not in error_msg and "模型:" not in error_msg: if "使用的模型:" not in error_msg and "模型:" not in error_msg:
model_info = f"使用的模型: {llm_config.model_name} (ID: {llm_config.id}), base_url: {llm_config.base_url}" model_info = (
f"使用的模型: {llm_config.model_name} (ID: {llm_config.id}), "
f"base_url: {llm_config.base_url}"
)
if "Not Found" in error_msg or "404" in error_msg: if "Not Found" in error_msg or "404" in error_msg:
detailed_error = f"{detailed_error}{model_info}。可能的原因1) 模型名称格式不正确SiliconFlow需要org/model格式2) base_url配置错误3) API端点不存在" detailed_error = (
elif "403" in error_msg or "account balance" in error_msg.lower() or "insufficient" in error_msg.lower(): f"{detailed_error}{model_info}。可能的原因1) 模型名称格式不正确SiliconFlow需要org/model格式"
detailed_error = f"{detailed_error}{model_info}。可能的原因账户余额不足或API密钥权限不足" "2) base_url配置错误3) API端点不存在"
)
elif (
"403" in error_msg
or "account balance" in error_msg.lower()
or "insufficient" in error_msg.lower()
):
detailed_error = (
f"{detailed_error}{model_info}。可能的原因账户余额不足或API密钥权限不足"
)
elif "401" in error_msg or "authentication" in error_msg.lower(): elif "401" in error_msg or "authentication" in error_msg.lower():
detailed_error = f"{detailed_error}{model_info}。可能的原因API密钥无效或已过期" detailed_error = (
f"{detailed_error}{model_info}。可能的原因API密钥无效或已过期"
)
else: else:
detailed_error = f"{detailed_error}{model_info}" detailed_error = f"{detailed_error}{model_info}"
logger.error(f"LLM调用失败: {detailed_error}") logger.error(f"LLM调用失败: {detailed_error}")
# 如果当前使用的不是默认模型,并且错误包含 Not Found / 404则尝试回退到默认模型再调用一次 if (not used_default_model) and (
if (not used_default_model) and ("Not Found" in error_msg or "404" in error_msg): "Not Found" in error_msg or "404" in error_msg
):
try: try:
from ..services.llm_config_service import LLMConfigService from ..services.llm_config_service import LLMConfigService
llm_config_service = LLMConfigService() llm_config_service = LLMConfigService()
default_config = await llm_config_service.get_default_chat_config(self.session) default_config = await llm_config_service.get_default_chat_config(
self.session
)
if default_config: if default_config:
logger.warning( logger.warning(
f"LLM调用失败模型可能不存在或端点错误" "LLM调用失败模型可能不存在或端点错误"
f"尝试使用默认模型重试: {default_config.model_name} (ID: {default_config.id})" f"尝试使用默认模型重试: {default_config.model_name} (ID: {default_config.id})"
) )
fallback_response = await self.llm_service.chat_completion( fallback_response = await self.llm_service.chat_completion(
model_config=default_config, model_config=default_config,
messages=[{"role": "user", "content": prompt}], messages=[{"role": "user", "content": prompt}],
temperature=config.get('temperature', 0.7), temperature=config.get('temperature', 0.7),
max_tokens=config.get('max_tokens') max_tokens=config.get('max_tokens'),
) )
return { return {
'success': True, 'success': True,
'response': fallback_response, 'response': fallback_response,
'prompt': prompt, 'prompt': prompt,
'model': default_config.model_name, 'model': default_config.model_name,
'tokens_used': getattr(fallback_response, 'usage', {}).get('total_tokens', 0) 'tokens_used': getattr(
if hasattr(fallback_response, 'usage') else 0, fallback_response, 'usage', {}
'fallback_model_used': True ).get('total_tokens', 0)
if hasattr(fallback_response, 'usage')
else 0,
'fallback_model_used': True,
} }
except Exception as fallback_error: except Exception as fallback_error:
logger.error(f"使用默认模型重试LLM调用失败: {str(fallback_error)}") logger.error(
# 继续向下抛出原始错误 f"使用默认模型重试LLM调用失败: {str(fallback_error)}"
)
raise ValueError(f"LLM调用失败: {detailed_error}") raise ValueError(f"LLM调用失败: {detailed_error}")
async def _execute_llm_node_stream(self, execution: WorkflowExecution, node: Dict[str, Any], context: Dict[str, Any]):
"""执行LLM节点流式版本用于 /execute-stream 接口"""
node_id = node['id']
config = self._prepare_node_input(node, context).get('node_config', {})
# 下面的逻辑与 _execute_llm_node 中获取模型配置和提示词的过程保持一致,
# 以保证流式与非流式路径的行为一致。
used_default_model = False
# 获取LLM配置
model_id = config.get('model_id')
if not model_id:
model_value = config.get('model_name', config.get('model'))
if model_value:
if isinstance(model_value, int):
model_id = model_value
else:
from sqlalchemy import select
result = await self.session.execute(
select(LLMConfig).where(LLMConfig.model_name == model_value)
)
llm_cfg = result.scalar_one_or_none()
if llm_cfg:
model_id = llm_cfg.id
if not model_id:
node_config = node.get('config', {})
model_id = node_config.get('model_id')
if not model_id:
model_value = node_config.get('model_name', node_config.get('model'))
if model_value:
if isinstance(model_value, int):
model_id = model_value
else:
from sqlalchemy import select
result = await self.session.execute(
select(LLMConfig).where(LLMConfig.model_name == model_value)
)
llm_cfg = result.scalar_one_or_none()
if llm_cfg:
model_id = llm_cfg.id
if not model_id:
from ..services.llm_config_service import LLMConfigService
llm_config_service = LLMConfigService()
default_config = await llm_config_service.get_default_chat_config(self.session)
if default_config:
model_id = default_config.id
used_default_model = True
logger.info(f"[STREAM] LLM节点未指定模型配置使用默认模型: {default_config.model_name} (ID: {model_id})")
else:
raise ValueError(
"未指定有效的大模型配置,且未找到默认配置。"
)
from sqlalchemy import select
result = await self.session.execute(
select(LLMConfig).where(LLMConfig.id == model_id)
)
llm_config = result.scalar_one_or_none()
if not llm_config:
raise ValueError(f"大模型配置 {model_id} 不存在")
# 构造 prompt使用与非流式路径相同的逻辑
input_data = self._prepare_node_input(node, context)
config = input_data.get('node_config', {})
prompt, prompt_template = self._build_llm_prompt(node, input_data)
# 打印流式路径下的提示词,确认实际发给大模型的内容
logger.info(
f"LLM 节点最终提示词(流式): node_id={node.get('id')}, "
f"model_id={llm_config.id}, model_name={llm_config.model_name}, prompt={prompt}"
)
full_response = ""
try:
# 调用 LLMService 流式接口
async for chunk in self.llm_service.chat_completion_stream(
model_config=llm_config,
messages=[{"role": "user", "content": prompt}],
temperature=config.get('temperature', 0.7),
max_tokens=config.get('max_tokens')
):
if not chunk:
continue
full_response += chunk
# 将增量结果向外层生成器抛出
yield {
'event_type': 'delta',
'delta': chunk,
'full_response': full_response,
}
# 完成后抛出最终结果,供后续节点依赖
final_output = {
'success': True,
'response': full_response,
'prompt': prompt,
'model': llm_config.model_name,
'tokens_used': 0 # 流式接口暂不提供 usage 统计
}
yield {
'event_type': 'final',
'output': final_output,
}
except Exception as e:
error_msg = str(e)
detailed_error = error_msg
if "使用的模型:" not in error_msg and "模型:" not in error_msg:
model_info = f"使用的模型: {llm_config.model_name} (ID: {llm_config.id}), base_url: {llm_config.base_url}"
detailed_error = f"{detailed_error}{model_info}"
logger.error(f"[STREAM] LLM流式调用失败: {detailed_error}")
raise ValueError(f"LLM流式调用失败: {detailed_error}")
def _substitute_variables(self, template: str, input_data: Dict[str, Any]) -> str: def _substitute_variables(self, template: str, input_data: Dict[str, Any]) -> str:
"""变量替换函数""" """变量替换函数"""
import re import re