feat: 修复工具返回异常的问题

This commit is contained in:
Wenjie Zhang 2026-06-03 14:09:31 +08:00
parent 206ddd8ff0
commit eb55780d0d
4 changed files with 122 additions and 4 deletions

View File

@ -30,11 +30,11 @@
- [x] 子智能体的优化,参考 PR 的方案。
- [x] 附件上传能够支持转换为 PDF待办查看 OCR 模型的状态,样式优化,保存的文件名不对
- [x] 参考 PR实现内置 Dashscope 的 Embedding 和 rerank 的方法
- [ ] 优化知识库的 API 接口设计,使用 /{db_id}/xxx 的形式,整合 mindmap / eval 接口
- [x] 优化知识库的 API 接口设计,使用 /{db_id}/xxx 的形式,整合 mindmap / eval 接口
- [x] 前端新增基于 ID 的随机像素头像生成机制(类似 GitHub 方块头像),用于替代默认图片
- [x] 子智能体右上角功能增强任务按钮常驻展开后展示已激活子智能体、附件、state 等信息
- [ ] 流式输出混排修复ongoing 流式返回时按 thread ID 区分主/子智能体内容,子智能体内容单独可视化渲染
- [ ] 子智能体中间过程可查看:流式结束后,通过子智能体自身 thread ID 获取并渲染中间调用过程
- [x] 流式输出混排修复ongoing 流式返回时按 thread ID 区分主/子智能体内容,子智能体内容单独可视化渲染
- [x] 子智能体中间过程可查看:流式结束后,通过子智能体自身 thread ID 获取并渲染中间调用过程
- [x] allow multi-hop qa generate
- [x] 在工作区的文件编辑的时候,保存和取消的按钮应该是悬浮在编辑框的右上角,而不是在 header 上面
- [x] default enable all build in tools / kbs / skills / mcps / subagents
@ -44,7 +44,7 @@
- [ ] 考虑如何将知识库更好的挂载到沙盒,是不是可以使用一个别的后端,但是使用别的后端是否还能读取到数据?应该不能
- [x] 智能体体系改进。改进子智能体,我觉得子智能体并不是一个子级的智能体,应该是
- [ ] RAG 中的文件的 metadata 包含那些内容?然后 Find 和 Read 的时候要支持展示
- [ ] 检查前端的这些 store 的使用,是否有需要调整优化收敛的地方
- [x] 检查前端的这些 store 的使用,是否有需要调整优化收敛的地方
- [x] parser 从plugins 移动到 knowledge 里面guard 移动到services 里面
- [x] neo4j 相关的服务,可以移动到 storage 里面
- [ ] 点开对话的时候要能够自动定位到尾部,而不是最开始。

View File

@ -7,9 +7,11 @@ from contextlib import suppress
from pathlib import Path
from typing import Any
from langchain_core.messages import ToolMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver, aiosqlite
from langgraph.graph.state import CompiledStateGraph
from langgraph.types import Command
from yuxi import config as sys_config
from yuxi.agents.context import BaseContext, resolve_agent_resource_options
@ -30,6 +32,29 @@ def _json_safe(value: Any) -> Any:
return str(value)
def _normalize_tool_event_data(data: Any) -> Any:
"""规整 tools 流事件write_todos / task 等返回 Command 的工具,其 tool-finished
output Command 对象_json_safe 只能退化成 repr 字符串前端无法关联结果
这里从 Command.update["messages"] 取出真正的 ToolMessage使其与普通工具一致"""
if not isinstance(data, dict) or data.get("event") != "tool-finished":
return data
output = data.get("output")
if not isinstance(output, Command):
return data
update = output.update if isinstance(output.update, dict) else {}
messages = update.get("messages")
if not isinstance(messages, list):
return data
tool_call_id = data.get("tool_call_id")
tool_message = next(
(m for m in messages if isinstance(m, ToolMessage) and m.tool_call_id == tool_call_id),
next((m for m in messages if isinstance(m, ToolMessage)), None),
)
if tool_message is None:
return data
return {**data, "output": tool_message}
def _metadata_thread_id(value: Any) -> str | None:
if not isinstance(value, dict):
return None
@ -235,6 +260,8 @@ class BaseAgent:
elif method == "values" and not namespace:
yield "values", data
elif method in {"tasks", "tools", "lifecycle"}:
if method == "tools":
data = _normalize_tool_event_data(data)
event_payload = {
"method": method,
"namespace": namespace,

View File

@ -0,0 +1,60 @@
from __future__ import annotations
from langchain_core.messages import ToolMessage
from langgraph.types import Command
from yuxi.agents.base import _json_safe, _normalize_tool_event_data
def _command_tool_finished(tool_call_id: str) -> dict:
"""模拟 write_todos / task 这类返回 Command 的工具的 tool-finished 事件。"""
tool_message = ToolMessage(
content="Updated todo list to [{'content': '步骤一', 'status': 'in_progress'}]",
tool_call_id=tool_call_id,
)
command = Command(update={"todos": [{"content": "步骤一", "status": "in_progress"}], "messages": [tool_message]})
return {"event": "tool-finished", "tool_call_id": tool_call_id, "output": command}
def test_command_tool_finished_extracts_tool_message_for_frontend_association():
tool_call_id = "call_abc"
data = _normalize_tool_event_data(_command_tool_finished(tool_call_id))
safe = _json_safe(data)
output = safe["output"]
# 前端按 tool_call_id 关联结果,并要求 output 是对象dict否则会被丢弃。
assert isinstance(output, dict)
assert output["tool_call_id"] == tool_call_id
assert output["type"] == "tool"
assert "步骤一" in output["content"]
def test_command_tool_finished_prefers_message_matching_tool_call_id():
other = ToolMessage(content="别的工具结果", tool_call_id="call_other")
target = ToolMessage(content="目标结果", tool_call_id="call_target")
data = {
"event": "tool-finished",
"tool_call_id": "call_target",
"output": Command(update={"messages": [other, target]}),
}
output = _normalize_tool_event_data(data)["output"]
assert isinstance(output, ToolMessage)
assert output.tool_call_id == "call_target"
assert output.content == "目标结果"
def test_regular_dict_output_is_left_untouched():
data = {"event": "tool-finished", "tool_call_id": "call_x", "output": {"content": "plain", "type": "tool"}}
assert _normalize_tool_event_data(data)["output"] == {"content": "plain", "type": "tool"}
def test_tool_started_event_is_left_untouched():
data = {"event": "tool-started", "tool_call_id": "call_x", "output": None}
assert _normalize_tool_event_data(data) is data
def test_command_without_tool_message_is_left_untouched():
command = Command(update={"todos": [{"content": "无消息", "status": "pending"}]})
data = {"event": "tool-finished", "tool_call_id": "call_x", "output": command}
assert _normalize_tool_event_data(data)["output"] is command

View File

@ -59,6 +59,23 @@ const loadingMessageChunk = (chunk) => {
return msg || null
}
// 工具结果不走 messages 流,而是以 method=tools 的 stream_event 事件返回tool-started/tool-finished
// 取出 tool-finished 的 output一条 ToolMessage 字典),交给 msgChunks 与 AI 消息按 tool_call_id 关联。
const toolFinishedMessage = (chunk) => {
const streamEvent = chunk?.event
if (!streamEvent || streamEvent.method !== 'tools') return null
const data = streamEvent.data
if (!data || data.event !== 'tool-finished') return null
const output = data.output
if (!output || typeof output !== 'object') return null
const id = output.id || output.tool_call_id || data.tool_call_id
if (!id) return null
return { ...output, type: 'tool', id }
}
export function useAgentStreamHandler({
getThreadState,
processApprovalInStream,
@ -119,6 +136,20 @@ export function useAgentStreamHandler({
}
return false
case 'stream_event':
{
// 工具结果需立即落地(不经平滑层),写入 msgChunks 后由 convertToolResultToMessages
// 按 tool_call_id 关联到对应 AI 消息的 tool_call驱动其完成态。
const toolMessage = toolFinishedMessage(chunk)
if (toolMessage) {
if (!threadState.onGoingConv.msgChunks[toolMessage.id]) {
threadState.onGoingConv.msgChunks[toolMessage.id] = []
}
threadState.onGoingConv.msgChunks[toolMessage.id].push(toolMessage)
}
}
return false
case 'error':
streamSmoother?.flushThread(threadId)
handleChatError({ message: chunkMessage }, 'stream')