Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions apps/application/workflow/nodes/llm_node/llm_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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 {}
Expand All @@ -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(
Expand All @@ -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"),
Expand Down
11 changes: 11 additions & 0 deletions apps/application/workflow/nodes/llm_node/message_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions ui/src/workflow-canvas/config/node-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
Loading