From c8209289c244be7ae241d44318f4895ad85519c0 Mon Sep 17 00:00:00 2001 From: hexiaonan-800 Date: Fri, 30 Oct 2026 15:47:39 +0800 Subject: [PATCH] feat: LLM node add reasoning key --- .../workflow/nodes/llm_node/llm_node.py | 15 +++++++++++---- .../workflow/nodes/llm_node/message_converter.py | 11 +++++++++++ ui/src/workflow-canvas/config/node-data.ts | 1 + 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/apps/application/workflow/nodes/llm_node/llm_node.py b/apps/application/workflow/nodes/llm_node/llm_node.py index d0b1719e2fe..373aacc623d 100644 --- a/apps/application/workflow/nodes/llm_node/llm_node.py +++ b/apps/application/workflow/nodes/llm_node/llm_node.py @@ -4,7 +4,7 @@ @Author:虎虎虎 @file: llm_node.py @date:2026/10/16 -@desc: LLM 节点:单次调用大模型(不执行工具),输出 content/reasoning_content/refusal/is_refusal/usage/tool_calls/finish_reason +@desc: LLM 节点:单次调用大模型(不执行工具),输出 content/reasoning_content/reasoning_key/refusal/is_refusal/usage/tool_calls/finish_reason """ import asyncio @@ -24,7 +24,7 @@ from application.workflow.message.struct.reasoning_content import ReasoningContent from application.workflow.message.struct.text_content import TextContent from application.workflow.message.struct.tool_content import ToolContent -from application.workflow.nodes.llm_node.message_converter import to_messages +from application.workflow.nodes.llm_node.message_converter import to_messages, to_reasoning_chunk from application.workflow.status import Status from common.exception.app_exception import AppApiException from common.utils.count_tokens import count_local_tokens @@ -255,6 +255,7 @@ async def _stream_response(self, model, chat_model, message_list): text_content_id = str(uuid.uuid7()) tool_call_accumulator = ToolCallAccumulator() reasoning_content = "" + reasoning_key = "" reasoning_end = False full = None finish_reason = "error" @@ -263,8 +264,9 @@ async def _stream_response(self, model, chat_model, message_list): async for chunk in response: self._check_cancelled() full = chunk if full is None else full + chunk - reasoning_chunk = chunk.additional_kwargs.get("reasoning_content") + key, reasoning_chunk = to_reasoning_chunk(chunk) if reasoning_chunk and not reasoning_end: + reasoning_key = reasoning_key or key reasoning_content += reasoning_chunk self._output( ReasoningContent(reasoning_content_id, reasoning_chunk, Status.RUNNING, node_info, position) @@ -317,13 +319,16 @@ async def _stream_response(self, model, chat_model, message_list): message_list, full, reasoning_content, + reasoning_key, finish_reason, tool_call_accumulator.tool_calls(), ) with contextlib.suppress(Exception): await response.aclose() - def _write_final_context(self, chat_model, message_list, message, reasoning_content, finish_reason, tool_calls): + def _write_final_context( + self, chat_model, message_list, message, reasoning_content, reasoning_key, finish_reason, tool_calls + ): content = message.content if message is not None and isinstance(message.content, str) else "" additional_kwargs = message.additional_kwargs if message is not None else {} response_metadata = message.response_metadata if message is not None else {} @@ -346,6 +351,7 @@ def _write_final_context(self, chat_model, message_list, message, reasoning_cont finish_reason = "tool_calls" if tool_calls else "stop" self.write_context("content", content) self.write_context("reasoning_content", reasoning_content) + self.write_context("reasoning_key", reasoning_key) self.write_context("refusal", refusal) self.write_context("is_refusal", bool(refusal)) self.write_context( @@ -371,6 +377,7 @@ def get_details(self, index: int = 0, position: dict = None, old_details: dict = "message_list": self.get_context("message_list"), "content": self.get_context("content"), "reasoning_content": self.get_context("reasoning_content"), + "reasoning_key": self.get_context("reasoning_key"), "refusal": self.get_context("refusal"), "is_refusal": self.get_context("is_refusal"), "usage": self.get_context("usage"), diff --git a/apps/application/workflow/nodes/llm_node/message_converter.py b/apps/application/workflow/nodes/llm_node/message_converter.py index bd10763e1fd..d7f90e3bbaf 100644 --- a/apps/application/workflow/nodes/llm_node/message_converter.py +++ b/apps/application/workflow/nodes/llm_node/message_converter.py @@ -39,6 +39,8 @@ OPENAI_MESSAGE_KEYS = ("role", "content", "name", "tool_calls", "tool_call_id") TOOL_NAME_KEYS = ("name", "tool_name", "toolName", "function_name") TOOL_ARGUMENT_KEYS = ("arguments", "function_arguments", "functionArguments") +# 各家模型输出思考内容的字段名不同,按优先级识别;reasoning_key 输出实际命中的字段,便于下游按原字段回传 +REASONING_KEYS = ("reasoning_content", "reasoning", "reasoning_text", "thinking") def _content_type(item: dict): @@ -208,3 +210,12 @@ def to_messages(value) -> list[BaseMessage]: if not isinstance(value, list): return [] return _Converter().convert(value) + + +def to_reasoning_chunk(chunk): + """从分片中取思考内容,返回 (实际使用的字段名, 内容);无思考内容时返回 (None, None)""" + for key in REASONING_KEYS: + value = chunk.additional_kwargs.get(key) + if value: + return key, value + return None, None diff --git a/ui/src/workflow-canvas/config/node-data.ts b/ui/src/workflow-canvas/config/node-data.ts index b377bb5a13f..b26b367f099 100644 --- a/ui/src/workflow-canvas/config/node-data.ts +++ b/ui/src/workflow-canvas/config/node-data.ts @@ -133,6 +133,7 @@ export const llmNode = { fields: [ { label: '回答内容', value: 'content' }, { label: '思考过程', value: 'reasoning_content' }, + { label: '思考标识', value: 'reasoning_key' }, { label: '拒答内容', value: 'refusal' }, { label: '是否拒答', value: 'is_refusal' }, { label: 'Token 用量', value: 'usage' },