in devel
This commit is contained in:
parent
169666443f
commit
3640f3c216
|
|
@ -1,9 +1,11 @@
|
||||||
"""
|
"""
|
||||||
client/agent_client.py
|
client/agent_client.py
|
||||||
Agent 客户端:驱动完整 OpenAI Function Calling + Tool Chain 执行流程
|
修复:OpenAI 消息序列必须满足
|
||||||
新增: OpenAI 格式消息序列管理,支持多轮工具调用上下文传递
|
user → assistant(tool_calls) → tool(s) → assistant(reply)
|
||||||
|
每个 tool 消息的 tool_call_id 必须与同一 assistant 消息中的 tool_calls[].id 完全匹配
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
@ -39,23 +41,31 @@ class AgentClient:
|
||||||
"""
|
"""
|
||||||
Agent 客户端:OpenAI Function Calling + Multi-Step Tool Chain
|
Agent 客户端:OpenAI Function Calling + Multi-Step Tool Chain
|
||||||
|
|
||||||
执行流程:
|
✅ 修复后的 OpenAI 消息序列规范:
|
||||||
1. [CLIENT] 接收用户输入,写入 Memory
|
|
||||||
2. [LLM] plan_tool_chain() → OpenAI Function Calling → ChainPlan
|
|
||||||
3. [CHAIN] 串行执行每个 ToolStep:
|
|
||||||
a. 构造 MCPRequest → MCPServer 执行工具
|
|
||||||
b. 将工具结果追加到 OpenAI 消息序列(tool role)
|
|
||||||
c. 记录 StepResult,更新链路上下文(占位符替换)
|
|
||||||
4. [LLM] generate_chain_reply() → OpenAI 整合结果 → 最终回复
|
|
||||||
5. [MEMORY] 写入完整调用链记录
|
|
||||||
|
|
||||||
OpenAI 消息序列示例(多步骤):
|
【单步工具调用】
|
||||||
{"role": "system", "content": "规划器提示"}
|
{"role": "system", "content": "..."}
|
||||||
|
{"role": "user", "content": "计算 1+1"}
|
||||||
|
{"role": "assistant", "content": null,
|
||||||
|
"tool_calls": [{"id": "call_abc", "type": "function",
|
||||||
|
"function": {"name": "calculator", "arguments": "{...}"}}]}
|
||||||
|
{"role": "tool", "content": "结果: 2", "tool_call_id": "call_abc"}
|
||||||
|
→ generate_reply() → 最终回复
|
||||||
|
|
||||||
|
【多步工具调用(并行声明,串行执行)】
|
||||||
|
{"role": "system", "content": "..."}
|
||||||
{"role": "user", "content": "搜索天气然后计算..."}
|
{"role": "user", "content": "搜索天气然后计算..."}
|
||||||
{"role": "assistant", "tool_calls": [{web_search}, {calculator}]}
|
{"role": "assistant", "content": null,
|
||||||
{"role": "tool", "content": "web_search 结果", "tool_call_id": "call_1"}
|
"tool_calls": [{"id": "call_001", ...web_search...},
|
||||||
{"role": "tool", "content": "calculator 结果", "tool_call_id": "call_2"}
|
{"id": "call_002", ...calculator...}]}
|
||||||
→ generate_reply() → 最终自然语言回复
|
{"role": "tool", "content": "搜索结果", "tool_call_id": "call_001"}
|
||||||
|
{"role": "tool", "content": "计算结果", "tool_call_id": "call_002"}
|
||||||
|
→ generate_reply() → 最终回复
|
||||||
|
|
||||||
|
⚠️ 关键约束:
|
||||||
|
1. tool 消息必须紧跟在含 tool_calls 的 assistant 消息之后
|
||||||
|
2. 每个 tool 的 tool_call_id 必须与 assistant.tool_calls[i].id 完全一致
|
||||||
|
3. tool_calls 中声明了几个工具,就必须有几条对应的 tool 消息
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -68,7 +78,6 @@ class AgentClient:
|
||||||
self.mcp_server = mcp_server
|
self.mcp_server = mcp_server
|
||||||
self.memory = memory
|
self.memory = memory
|
||||||
self.logger = get_logger("CLIENT")
|
self.logger = get_logger("CLIENT")
|
||||||
# OpenAI 格式的结构化对话历史(跨轮次保持上下文)
|
|
||||||
self._openai_history: list[dict] = []
|
self._openai_history: list[dict] = []
|
||||||
self.logger.info("💻 Agent Client 初始化完成(OpenAI Function Calling 模式)")
|
self.logger.info("💻 Agent Client 初始化完成(OpenAI Function Calling 模式)")
|
||||||
|
|
||||||
|
|
@ -83,12 +92,11 @@ class AgentClient:
|
||||||
self.logger.info(f"📨 收到用户输入: {user_input}")
|
self.logger.info(f"📨 收到用户输入: {user_input}")
|
||||||
self.logger.info(sep)
|
self.logger.info(sep)
|
||||||
|
|
||||||
# Step 1: 记录用户消息
|
|
||||||
self.memory.add_user_message(user_input)
|
self.memory.add_user_message(user_input)
|
||||||
context = self.memory.get_context_summary()
|
context = self.memory.get_context_summary()
|
||||||
|
|
||||||
# Step 2: LLM 规划工具调用链
|
# Step 1: LLM 规划工具调用链
|
||||||
self.logger.info("🗺 Step 2 [LLM] 规划工具调用链...")
|
self.logger.info("🗺 [LLM] 规划工具调用链...")
|
||||||
tool_schemas = self.mcp_server.get_tool_schemas()
|
tool_schemas = self.mcp_server.get_tool_schemas()
|
||||||
plan: ChainPlan = self.llm.plan_tool_chain(
|
plan: ChainPlan = self.llm.plan_tool_chain(
|
||||||
user_input=user_input,
|
user_input=user_input,
|
||||||
|
|
@ -101,11 +109,13 @@ class AgentClient:
|
||||||
if not plan.steps:
|
if not plan.steps:
|
||||||
return self._handle_direct_reply(user_input, context)
|
return self._handle_direct_reply(user_input, context)
|
||||||
|
|
||||||
# Step 3~4: 执行工具调用链,构造 OpenAI 消息序列
|
# Step 2: 执行工具调用链,构造合规的 OpenAI 消息序列
|
||||||
chain_result, tool_messages = self._execute_chain(plan, user_input)
|
chain_result, openai_tool_block = self._execute_chain(plan, user_input)
|
||||||
|
|
||||||
# Step 5: 调用 OpenAI 整合结果,生成最终回复
|
# Step 3: 调用 OpenAI 整合结果,生成最终回复
|
||||||
return self._generate_response(user_input, chain_result, tool_messages, context)
|
return self._generate_response(
|
||||||
|
user_input, chain_result, openai_tool_block, context
|
||||||
|
)
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 串行执行引擎
|
# 串行执行引擎
|
||||||
|
|
@ -117,12 +127,20 @@ class AgentClient:
|
||||||
user_input: str,
|
user_input: str,
|
||||||
) -> tuple[ChainResult, list[dict]]:
|
) -> tuple[ChainResult, list[dict]]:
|
||||||
"""
|
"""
|
||||||
串行执行工具调用链,同步构造 OpenAI 消息序列
|
串行执行工具调用链,构造符合 OpenAI 协议的消息块
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(ChainResult, tool_messages)
|
chain_result: 执行结果汇总
|
||||||
tool_messages 为 OpenAI 格式的工具调用消息列表,
|
openai_tool_block: 合规的 OpenAI 消息块,结构为:
|
||||||
用于后续 generate_reply() 调用
|
[
|
||||||
|
assistant(tool_calls=[call_001, call_002, ...]),
|
||||||
|
tool(tool_call_id=call_001, content=...),
|
||||||
|
tool(tool_call_id=call_002, content=...),
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
⚠️ 关键:所有 tool_calls 在 assistant 消息中一次性声明,
|
||||||
|
然后逐条追加对应的 tool 结果消息,保证 id 完全匹配。
|
||||||
"""
|
"""
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"\n{'─' * 60}\n"
|
f"\n{'─' * 60}\n"
|
||||||
|
|
@ -132,21 +150,26 @@ class AgentClient:
|
||||||
f"{'─' * 60}"
|
f"{'─' * 60}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ── 1. 预先为每个步骤生成稳定的 tool_call_id ──────────
|
||||||
|
# 必须在执行前全部生成,assistant 消息和 tool 消息共享同一批 id
|
||||||
|
step_call_ids: dict[int, str] = {
|
||||||
|
step.step_id: f"call_{uuid.uuid4().hex[:12]}"
|
||||||
|
for step in plan.steps
|
||||||
|
}
|
||||||
|
self.logger.debug(f"🔑 预生成 tool_call_ids: {step_call_ids}")
|
||||||
|
|
||||||
|
# ── 2. 构造 assistant 消息(一次性声明全部 tool_calls)──
|
||||||
|
assistant_msg = self._build_assistant_message(plan, step_call_ids)
|
||||||
|
|
||||||
|
# ── 3. 串行执行每个步骤,收集 tool 结果消息 ────────────
|
||||||
step_results: list[StepResult] = []
|
step_results: list[StepResult] = []
|
||||||
|
tool_result_msgs: list[dict] = []
|
||||||
chain_context: dict[str, str] = {}
|
chain_context: dict[str, str] = {}
|
||||||
tool_messages: list[dict] = []
|
|
||||||
failed_step: int | None = None
|
failed_step: int | None = None
|
||||||
|
|
||||||
# 构造 assistant 消息(含 tool_calls 声明)
|
|
||||||
assistant_tool_calls = self._build_assistant_tool_calls(plan)
|
|
||||||
tool_messages.append({
|
|
||||||
"role": "assistant",
|
|
||||||
"content": None,
|
|
||||||
"tool_calls": assistant_tool_calls,
|
|
||||||
})
|
|
||||||
|
|
||||||
for step in plan.steps:
|
for step in plan.steps:
|
||||||
# 检查前置依赖
|
call_id = step_call_ids[step.step_id]
|
||||||
|
|
||||||
if self._has_failed_dependency(step, failed_step):
|
if self._has_failed_dependency(step, failed_step):
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
f"⏭ Step {step.step_id} [{step.tool_name}] 跳过"
|
f"⏭ Step {step.step_id} [{step.tool_name}] 跳过"
|
||||||
|
|
@ -159,25 +182,22 @@ class AgentClient:
|
||||||
output="",
|
output="",
|
||||||
error=f"跳过:依赖步骤 {failed_step} 失败",
|
error=f"跳过:依赖步骤 {failed_step} 失败",
|
||||||
))
|
))
|
||||||
# 向 OpenAI 消息序列写入失败占位
|
# ⚠️ 即使跳过,也必须补一条 tool 消息,保持 id 完全匹配
|
||||||
tool_messages.append({
|
tool_result_msgs.append({
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"content": f"步骤跳过:依赖步骤 {failed_step} 执行失败",
|
"content": f"步骤跳过:依赖步骤 {failed_step} 执行失败",
|
||||||
"tool_call_id": assistant_tool_calls[step.step_id - 1]["id"],
|
"tool_call_id": call_id,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 执行单步
|
result = self._execute_single_step(step, chain_context, call_id)
|
||||||
result, tool_call_id = self._execute_single_step(
|
|
||||||
step, chain_context, assistant_tool_calls
|
|
||||||
)
|
|
||||||
step_results.append(result)
|
step_results.append(result)
|
||||||
|
|
||||||
# 追加 tool 消息到 OpenAI 序列
|
# 追加 tool 结果消息(tool_call_id 与 assistant 声明完全一致)
|
||||||
tool_messages.append({
|
tool_result_msgs.append({
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"content": result.output if result.success else f"执行失败: {result.error}",
|
"content": result.output if result.success else f"执行失败: {result.error}",
|
||||||
"tool_call_id": tool_call_id,
|
"tool_call_id": call_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
if result.success:
|
if result.success:
|
||||||
|
|
@ -186,6 +206,22 @@ class AgentClient:
|
||||||
else:
|
else:
|
||||||
failed_step = step.step_id
|
failed_step = step.step_id
|
||||||
|
|
||||||
|
# ── 4. 组装合规的 OpenAI 消息块 ─────────────────────────
|
||||||
|
# 格式: [assistant(tool_calls), tool, tool, ...]
|
||||||
|
openai_tool_block = [assistant_msg] + tool_result_msgs
|
||||||
|
|
||||||
|
self.logger.debug("📦 OpenAI 消息块结构:")
|
||||||
|
for i, msg in enumerate(openai_tool_block):
|
||||||
|
role = msg["role"]
|
||||||
|
if role == "assistant":
|
||||||
|
ids = [tc["id"] for tc in (msg.get("tool_calls") or [])]
|
||||||
|
self.logger.debug(f" [{i}] assistant tool_calls.ids = {ids}")
|
||||||
|
else:
|
||||||
|
self.logger.debug(
|
||||||
|
f" [{i}] tool tool_call_id = {msg.get('tool_call_id')}"
|
||||||
|
f" content = {str(msg.get('content', ''))[:50]}..."
|
||||||
|
)
|
||||||
|
|
||||||
overall_success = failed_step is None
|
overall_success = failed_step is None
|
||||||
chain_result = ChainResult(
|
chain_result = ChainResult(
|
||||||
goal=plan.goal,
|
goal=plan.goal,
|
||||||
|
|
@ -200,34 +236,25 @@ class AgentClient:
|
||||||
f" 完成: {chain_result.completed_steps}/{chain_result.total_steps} 步\n"
|
f" 完成: {chain_result.completed_steps}/{chain_result.total_steps} 步\n"
|
||||||
f"{'─' * 60}"
|
f"{'─' * 60}"
|
||||||
)
|
)
|
||||||
return chain_result, tool_messages
|
return chain_result, openai_tool_block
|
||||||
|
|
||||||
def _execute_single_step(
|
def _execute_single_step(
|
||||||
self,
|
self,
|
||||||
step: ToolStep,
|
step: ToolStep,
|
||||||
chain_context: dict[str, str],
|
chain_context: dict[str, str],
|
||||||
assistant_tool_calls: list[dict],
|
call_id: str,
|
||||||
) -> tuple[StepResult, str]:
|
) -> StepResult:
|
||||||
"""
|
"""执行单个步骤,返回 StepResult"""
|
||||||
执行单个步骤,返回 (StepResult, tool_call_id)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
StepResult: 步骤执行结果
|
|
||||||
tool_call_id: 对应的 OpenAI tool_call_id(用于消息序列关联)
|
|
||||||
"""
|
|
||||||
# 注入前步上下文(占位符替换)
|
|
||||||
resolved_step = step.inject_context(chain_context)
|
resolved_step = step.inject_context(chain_context)
|
||||||
tool_call_id = assistant_tool_calls[step.step_id - 1]["id"]
|
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"\n ▶ Step {step.step_id} 执行中\n"
|
f"\n ▶ Step {step.step_id} 执行中\n"
|
||||||
f" 工具 : [{resolved_step.tool_name}]\n"
|
f" 工具 : [{resolved_step.tool_name}]\n"
|
||||||
f" 说明 : {resolved_step.description}\n"
|
f" 说明 : {resolved_step.description}\n"
|
||||||
f" 参数 : {resolved_step.arguments}\n"
|
f" 参数 : {resolved_step.arguments}\n"
|
||||||
f" call_id: {tool_call_id}"
|
f" call_id : {call_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 构造并发送 MCP 请求
|
|
||||||
mcp_request: MCPRequest = resolved_step.to_mcp_request()
|
mcp_request: MCPRequest = resolved_step.to_mcp_request()
|
||||||
mcp_response: MCPResponse = self.mcp_server.handle_request(mcp_request)
|
mcp_response: MCPResponse = self.mcp_server.handle_request(mcp_request)
|
||||||
|
|
||||||
|
|
@ -239,7 +266,7 @@ class AgentClient:
|
||||||
tool_name=step.tool_name,
|
tool_name=step.tool_name,
|
||||||
success=True,
|
success=True,
|
||||||
output=output,
|
output=output,
|
||||||
), tool_call_id
|
)
|
||||||
else:
|
else:
|
||||||
error_msg = mcp_response.error.get("message", "未知错误")
|
error_msg = mcp_response.error.get("message", "未知错误")
|
||||||
self.logger.error(f" ❌ Step {step.step_id} 失败: {error_msg}")
|
self.logger.error(f" ❌ Step {step.step_id} 失败: {error_msg}")
|
||||||
|
|
@ -249,7 +276,7 @@ class AgentClient:
|
||||||
success=False,
|
success=False,
|
||||||
output="",
|
output="",
|
||||||
error=error_msg,
|
error=error_msg,
|
||||||
), tool_call_id
|
)
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 回复生成
|
# 回复生成
|
||||||
|
|
@ -259,55 +286,44 @@ class AgentClient:
|
||||||
self,
|
self,
|
||||||
user_input: str,
|
user_input: str,
|
||||||
chain_result: ChainResult,
|
chain_result: ChainResult,
|
||||||
tool_messages: list[dict],
|
openai_tool_block: list[dict],
|
||||||
context: str,
|
context: str,
|
||||||
) -> AgentResponse:
|
) -> AgentResponse:
|
||||||
"""调用 OpenAI 整合工具结果,生成最终 AgentResponse"""
|
"""调用 OpenAI 整合工具结果,生成最终 AgentResponse"""
|
||||||
self.logger.info("✍️ Step 5 [LLM] 调用 OpenAI 生成最终回复...")
|
self.logger.info("✍️ [LLM] 调用 OpenAI 生成最终回复...")
|
||||||
|
|
||||||
chain_summary = self._build_chain_summary(chain_result)
|
chain_summary = self._build_chain_summary(chain_result)
|
||||||
|
|
||||||
# 单步走简洁路径
|
# 将合规的消息块传给 LLMEngine
|
||||||
if chain_result.total_steps == 1:
|
|
||||||
r = chain_result.step_results[0]
|
|
||||||
final_reply = self.llm.generate_final_reply(
|
|
||||||
user_input=user_input,
|
|
||||||
tool_name=r.tool_name,
|
|
||||||
tool_output=r.output,
|
|
||||||
context=context,
|
|
||||||
tool_call_id=tool_messages[-1].get("tool_call_id", "") if tool_messages else "",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# 多步走完整 OpenAI 消息序列路径
|
|
||||||
final_reply = self.llm.generate_chain_reply(
|
final_reply = self.llm.generate_chain_reply(
|
||||||
user_input=user_input,
|
user_input=user_input,
|
||||||
chain_summary=chain_summary,
|
chain_summary=chain_summary,
|
||||||
context=context,
|
context=context,
|
||||||
tool_messages=tool_messages,
|
openai_tool_block=openai_tool_block, # ← 传递完整合规消息块
|
||||||
)
|
)
|
||||||
|
|
||||||
chain_result.final_reply = final_reply
|
chain_result.final_reply = final_reply
|
||||||
|
|
||||||
# 更新 OpenAI 结构化历史(供下一轮使用)
|
# 更新跨轮次 OpenAI 历史(只保留 user/assistant 摘要,不含 tool 消息)
|
||||||
self._openai_history.append({"role": "user", "content": user_input})
|
self._openai_history.append({"role": "user", "content": user_input})
|
||||||
self._openai_history.append({"role": "assistant", "content": final_reply})
|
self._openai_history.append({"role": "assistant", "content": final_reply})
|
||||||
# 保留最近 10 轮
|
|
||||||
if len(self._openai_history) > 20:
|
if len(self._openai_history) > 20:
|
||||||
self._openai_history = self._openai_history[-20:]
|
self._openai_history = self._openai_history[-20:]
|
||||||
|
|
||||||
# 写入 Memory
|
|
||||||
if chain_result.total_steps > 1:
|
if chain_result.total_steps > 1:
|
||||||
self.memory.add_chain_result(chain_result)
|
self.memory.add_chain_result(chain_result)
|
||||||
else:
|
else:
|
||||||
self.memory.add_assistant_message(final_reply)
|
self.memory.add_assistant_message(final_reply)
|
||||||
|
|
||||||
self.logger.info("🎉 [CLIENT] 流程完成,回复已返回")
|
self.logger.info("🎉 流程完成,回复已返回")
|
||||||
return AgentResponse(
|
return AgentResponse(
|
||||||
user_input=user_input,
|
user_input=user_input,
|
||||||
final_reply=final_reply,
|
final_reply=final_reply,
|
||||||
chain_result=chain_result,
|
chain_result=chain_result,
|
||||||
tool_used=chain_result.step_results[0].tool_name if chain_result.total_steps == 1 else None,
|
tool_used=(chain_result.step_results[0].tool_name
|
||||||
tool_output=chain_result.step_results[0].output if chain_result.total_steps == 1 else None,
|
if chain_result.total_steps == 1 else None),
|
||||||
|
tool_output=(chain_result.step_results[0].output
|
||||||
|
if chain_result.total_steps == 1 else None),
|
||||||
success=chain_result.success,
|
success=chain_result.success,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -321,42 +337,52 @@ class AgentClient:
|
||||||
return AgentResponse(user_input=user_input, final_reply=reply)
|
return AgentResponse(user_input=user_input, final_reply=reply)
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 工具方法
|
# 消息构造工具方法
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_assistant_tool_calls(plan: ChainPlan) -> list[dict]:
|
def _build_assistant_message(
|
||||||
|
plan: ChainPlan,
|
||||||
|
step_call_ids: dict[int, str],
|
||||||
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
构造 OpenAI assistant 消息中的 tool_calls 字段
|
构造 assistant 消息,一次性声明全部 tool_calls
|
||||||
|
|
||||||
格式:
|
✅ 正确格式:
|
||||||
[
|
|
||||||
{
|
{
|
||||||
"id": "call_abc123",
|
"role": "assistant",
|
||||||
|
"content": null,
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call_abc123", ← 与后续 tool 消息的 tool_call_id 完全一致
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "calculator",
|
"name": "calculator",
|
||||||
"arguments": '{"expression": "1+2"}'
|
"arguments": "{\"expression\": \"1+2\"}" ← 必须是 JSON 字符串
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
for step in plan.steps:
|
for step in plan.steps:
|
||||||
tool_calls.append({
|
tool_calls.append({
|
||||||
"id": f"call_{uuid.uuid4().hex[:8]}",
|
"id": step_call_ids[step.step_id],
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": step.tool_name,
|
"name": step.tool_name,
|
||||||
"arguments": json.dumps(step.arguments, ensure_ascii=False),
|
"arguments": json.dumps(step.arguments, ensure_ascii=False),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return tool_calls
|
return {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": tool_calls,
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_chain_summary(chain_result: ChainResult) -> str:
|
def _build_chain_summary(chain_result: ChainResult) -> str:
|
||||||
"""将调用链结果格式化为 LLM 可读的摘要"""
|
"""将调用链结果格式化为 LLM 可读的摘要(降级时使用)"""
|
||||||
lines = []
|
lines = []
|
||||||
for r in chain_result.step_results:
|
for r in chain_result.step_results:
|
||||||
if r.success:
|
if r.success:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
llm/llm_engine.py
|
llm/llm_engine.py
|
||||||
LLM 引擎:通过 Provider 接口调用真实 OpenAI API
|
修复:generate_chain_reply / generate_final_reply 消息序列构造
|
||||||
支持 Function Calling 多步骤规划 + 工具结果整合回复
|
确保 tool 消息始终紧跟在含 tool_calls 的 assistant 消息之后
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
@ -35,25 +35,26 @@ class ToolDecision:
|
||||||
|
|
||||||
class LLMEngine:
|
class LLMEngine:
|
||||||
"""
|
"""
|
||||||
LLM 推理引擎(Provider 模式)
|
LLM 推理引擎
|
||||||
|
|
||||||
核心流程:
|
✅ 修复后的消息序列规范:
|
||||||
1. plan_tool_chain()
|
|
||||||
构造 OpenAI 格式消息 + tools
|
|
||||||
→ Provider.plan_with_tools()
|
|
||||||
→ 解析 tool_calls → ChainPlan
|
|
||||||
|
|
||||||
2. generate_chain_reply()
|
generate_chain_reply() 构造的完整消息:
|
||||||
构造含工具结果的完整消息历史
|
[
|
||||||
→ Provider.generate_reply()
|
{"role": "system", "content": REPLY_SYSTEM_PROMPT},
|
||||||
→ 最终自然语言回复
|
{"role": "user", "content": "用户输入"},
|
||||||
|
# ↓ openai_tool_block(来自 AgentClient._execute_chain)
|
||||||
|
{"role": "assistant", "content": null,
|
||||||
|
"tool_calls": [{"id":"call_001","type":"function","function":{...}},
|
||||||
|
{"id":"call_002","type":"function","function":{...}}]},
|
||||||
|
{"role": "tool", "content": "结果1", "tool_call_id": "call_001"},
|
||||||
|
{"role": "tool", "content": "结果2", "tool_call_id": "call_002"},
|
||||||
|
]
|
||||||
|
→ provider.generate_reply() → 最终回复
|
||||||
|
|
||||||
降级策略:
|
⚠️ 绝对不能在 tool 消息前插入任何其他消息(尤其是 system/user)
|
||||||
API 调用失败 且 fallback_to_rules=true
|
|
||||||
→ 自动切换到规则引擎(保证系统可用性)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 规则引擎关键词(降级时使用)
|
|
||||||
_MULTI_STEP_KEYWORDS = [
|
_MULTI_STEP_KEYWORDS = [
|
||||||
"然后", "接着", "再", "并且", "同时", "之后",
|
"然后", "接着", "再", "并且", "同时", "之后",
|
||||||
"先.*再", "首先.*然后", "搜索.*计算", "读取.*执行",
|
"先.*再", "首先.*然后", "搜索.*计算", "读取.*执行",
|
||||||
|
|
@ -75,13 +76,12 @@ class LLMEngine:
|
||||||
self.logger.info(f" fallback_rules = {settings.agent.fallback_to_rules}")
|
self.logger.info(f" fallback_rules = {settings.agent.fallback_to_rules}")
|
||||||
|
|
||||||
def reconfigure(self, cfg: LLMConfig) -> None:
|
def reconfigure(self, cfg: LLMConfig) -> None:
|
||||||
"""热更新配置并重建 Provider"""
|
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
self.provider = create_provider(cfg)
|
self.provider = create_provider(cfg)
|
||||||
self.logger.info(f"🔄 LLM 配置已更新: model={cfg.model_name}")
|
self.logger.info(f"🔄 LLM 配置已更新: model={cfg.model_name}")
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 核心接口
|
# 工具规划
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def plan_tool_chain(
|
def plan_tool_chain(
|
||||||
|
|
@ -91,35 +91,16 @@ class LLMEngine:
|
||||||
context: str = "",
|
context: str = "",
|
||||||
history: list[dict] | None = None,
|
history: list[dict] | None = None,
|
||||||
) -> ChainPlan:
|
) -> ChainPlan:
|
||||||
"""
|
"""使用 OpenAI Function Calling 规划工具调用链"""
|
||||||
使用 OpenAI Function Calling 规划工具调用链
|
|
||||||
|
|
||||||
消息构造策略:
|
|
||||||
system → 规划器系统提示
|
|
||||||
history → 历史对话(可选)
|
|
||||||
user → 当前用户输入
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_input: 用户输入文本
|
|
||||||
tool_schemas: 可用工具列表
|
|
||||||
context: 对话历史摘要(文本格式,用于无 history 时)
|
|
||||||
history: 结构化对话历史(OpenAI 消息格式,优先使用)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ChainPlan 实例
|
|
||||||
"""
|
|
||||||
self.logger.info(f"🗺 规划工具调用链: {user_input[:60]}...")
|
self.logger.info(f"🗺 规划工具调用链: {user_input[:60]}...")
|
||||||
|
|
||||||
# 构造消息列表
|
|
||||||
messages = self._build_plan_messages(user_input, context, history)
|
messages = self._build_plan_messages(user_input, context, history)
|
||||||
|
|
||||||
if self.cfg.function_calling:
|
if self.cfg.function_calling:
|
||||||
# ── 真实 OpenAI Function Calling ──────────────────
|
|
||||||
result = self.provider.plan_with_tools(messages, tool_schemas)
|
result = self.provider.plan_with_tools(messages, tool_schemas)
|
||||||
|
|
||||||
if result.success and result.plan is not None:
|
if result.success and result.plan is not None:
|
||||||
plan = result.plan
|
plan = result.plan
|
||||||
# 补充 goal 字段
|
|
||||||
if not plan.goal:
|
if not plan.goal:
|
||||||
plan.goal = user_input
|
plan.goal = user_input
|
||||||
self.logger.info(f"📋 OpenAI 规划完成: {plan.step_count} 步")
|
self.logger.info(f"📋 OpenAI 规划完成: {plan.step_count} 步")
|
||||||
|
|
@ -130,15 +111,12 @@ class LLMEngine:
|
||||||
)
|
)
|
||||||
return plan
|
return plan
|
||||||
|
|
||||||
# API 失败处理
|
|
||||||
self.logger.warning(f"⚠️ OpenAI 规划失败: {result.error}")
|
self.logger.warning(f"⚠️ OpenAI 规划失败: {result.error}")
|
||||||
if settings.agent.fallback_to_rules:
|
if settings.agent.fallback_to_rules:
|
||||||
self.logger.info("🔄 降级到规则引擎...")
|
self.logger.info("🔄 降级到规则引擎...")
|
||||||
return self._rule_based_plan(user_input)
|
return self._rule_based_plan(user_input)
|
||||||
return ChainPlan(goal=user_input, steps=[])
|
return ChainPlan(goal=user_input, steps=[])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# function_calling=false 时直接使用规则引擎
|
|
||||||
self.logger.info("⚙️ function_calling=false,使用规则引擎")
|
self.logger.info("⚙️ function_calling=false,使用规则引擎")
|
||||||
return self._rule_based_plan(user_input)
|
return self._rule_based_plan(user_input)
|
||||||
|
|
||||||
|
|
@ -148,7 +126,6 @@ class LLMEngine:
|
||||||
tool_schemas: list[ToolSchema],
|
tool_schemas: list[ToolSchema],
|
||||||
context: str = "",
|
context: str = "",
|
||||||
) -> ToolDecision:
|
) -> ToolDecision:
|
||||||
"""单步工具决策(代理到 plan_tool_chain)"""
|
|
||||||
plan = self.plan_tool_chain(user_input, tool_schemas, context)
|
plan = self.plan_tool_chain(user_input, tool_schemas, context)
|
||||||
if not plan.steps:
|
if not plan.steps:
|
||||||
return ToolDecision(need_tool=False, reasoning="无需工具,直接回复")
|
return ToolDecision(need_tool=False, reasoning="无需工具,直接回复")
|
||||||
|
|
@ -160,37 +137,56 @@ class LLMEngine:
|
||||||
reasoning=first.description,
|
reasoning=first.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# 回复生成(核心修复区域)
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def generate_chain_reply(
|
def generate_chain_reply(
|
||||||
self,
|
self,
|
||||||
user_input: str,
|
user_input: str,
|
||||||
chain_summary: str,
|
chain_summary: str,
|
||||||
context: str = "",
|
context: str = "",
|
||||||
tool_messages: list[dict] | None = None,
|
openai_tool_block: list[dict] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
整合多步骤执行结果,调用 OpenAI 生成最终自然语言回复
|
整合工具执行结果,调用 OpenAI 生成最终自然语言回复
|
||||||
|
|
||||||
消息构造(含工具执行结果):
|
✅ 修复后消息序列:
|
||||||
system → 回复生成系统提示
|
system → 回复生成提示
|
||||||
user → 原始用户输入
|
user → 原始用户输入
|
||||||
assistant → 工具调用决策(tool_calls)
|
← openai_tool_block 直接追加(已包含 assistant+tool 消息)→
|
||||||
tool → 工具执行结果
|
assistant(tool_calls=[...])
|
||||||
...(多轮工具调用)
|
tool(tool_call_id=..., content=...)
|
||||||
|
tool(tool_call_id=..., content=...)
|
||||||
|
...
|
||||||
|
|
||||||
|
❌ 修复前的错误(导致 400):
|
||||||
|
system → user → tool → tool ← tool 前缺少 assistant(tool_calls)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_input: 原始用户输入
|
user_input: 原始用户输入
|
||||||
chain_summary: 步骤摘要(API 失败时的降级内容)
|
chain_summary: 步骤摘要(API 失败时的降级内容)
|
||||||
context: 对话历史
|
context: 对话历史(仅规划阶段使用,回复阶段不注入)
|
||||||
tool_messages: 完整的工具调用消息序列(OpenAI 格式)
|
openai_tool_block: 由 AgentClient 构造的合规消息块
|
||||||
|
格式: [assistant(tool_calls), tool, tool, ...]
|
||||||
Returns:
|
|
||||||
最终回复字符串
|
|
||||||
"""
|
"""
|
||||||
self.logger.info("✍️ 生成最终回复...")
|
self.logger.info("✍️ 生成最终回复(工具调用链模式)...")
|
||||||
|
|
||||||
|
if not openai_tool_block:
|
||||||
|
self.logger.warning("⚠️ openai_tool_block 为空,降级到摘要模板")
|
||||||
|
return self._fallback_chain_reply(user_input, chain_summary)
|
||||||
|
|
||||||
|
# 验证消息块合规性
|
||||||
|
if not self._validate_tool_block(openai_tool_block):
|
||||||
|
self.logger.warning("⚠️ 消息块验证失败,降级到摘要模板")
|
||||||
|
return self._fallback_chain_reply(user_input, chain_summary)
|
||||||
|
|
||||||
|
# ✅ 正确的消息序列构造
|
||||||
|
messages = self._build_reply_messages_with_block(user_input, openai_tool_block)
|
||||||
|
|
||||||
|
self.logger.debug(f"📤 发送回复请求,消息数: {len(messages)}")
|
||||||
|
self._log_messages_structure(messages)
|
||||||
|
|
||||||
if tool_messages:
|
|
||||||
# 构造含工具结果的完整消息历史
|
|
||||||
messages = self._build_reply_messages(user_input, tool_messages)
|
|
||||||
result = self.provider.generate_reply(messages)
|
result = self.provider.generate_reply(messages)
|
||||||
|
|
||||||
if result.success and result.content:
|
if result.success and result.content:
|
||||||
|
|
@ -200,8 +196,6 @@ class LLMEngine:
|
||||||
return result.content
|
return result.content
|
||||||
|
|
||||||
self.logger.warning(f"⚠️ OpenAI 回复生成失败: {result.error}")
|
self.logger.warning(f"⚠️ OpenAI 回复生成失败: {result.error}")
|
||||||
|
|
||||||
# 降级:使用模板回复
|
|
||||||
return self._fallback_chain_reply(user_input, chain_summary)
|
return self._fallback_chain_reply(user_input, chain_summary)
|
||||||
|
|
||||||
def generate_final_reply(
|
def generate_final_reply(
|
||||||
|
|
@ -212,29 +206,57 @@ class LLMEngine:
|
||||||
context: str = "",
|
context: str = "",
|
||||||
tool_call_id: str = "",
|
tool_call_id: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""单步工具结果整合(调用 OpenAI 生成自然语言回复)"""
|
"""
|
||||||
|
单步工具结果整合(调用 OpenAI 生成自然语言回复)
|
||||||
|
|
||||||
|
✅ 修复后消息序列:
|
||||||
|
system → 回复生成提示
|
||||||
|
user → 原始用户输入
|
||||||
|
assistant → tool_calls=[{id: tool_call_id, function: {name, arguments}}]
|
||||||
|
tool → content=tool_output, tool_call_id=tool_call_id
|
||||||
|
|
||||||
|
❌ 修复前的错误:
|
||||||
|
system → user → tool ← 缺少 assistant(tool_calls) 前置消息
|
||||||
|
"""
|
||||||
self.logger.info(f"✍️ 整合单步工具结果 [{tool_name}]...")
|
self.logger.info(f"✍️ 整合单步工具结果 [{tool_name}]...")
|
||||||
|
|
||||||
# 构造单步工具消息
|
if not tool_call_id:
|
||||||
tool_messages = []
|
# 无 tool_call_id 时降级到直接回复模式
|
||||||
if tool_call_id:
|
self.logger.warning("⚠️ tool_call_id 为空,使用直接回复模式")
|
||||||
tool_messages = [
|
return self._generate_simple_reply(user_input, tool_name, tool_output)
|
||||||
|
|
||||||
|
import json
|
||||||
|
# 构造单步合规消息块
|
||||||
|
single_tool_block = [
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": tool_call_id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": tool_name,
|
||||||
|
"arguments": json.dumps({"result": tool_output[:100]},
|
||||||
|
ensure_ascii=False),
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"content": tool_output,
|
"content": tool_output,
|
||||||
"tool_call_id": tool_call_id,
|
"tool_call_id": tool_call_id,
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return self.generate_chain_reply(
|
return self.generate_chain_reply(
|
||||||
user_input=user_input,
|
user_input=user_input,
|
||||||
chain_summary=tool_output,
|
chain_summary=tool_output,
|
||||||
context=context,
|
context=context,
|
||||||
tool_messages=tool_messages,
|
openai_tool_block=single_tool_block,
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_direct_reply(self, user_input: str, context: str = "") -> str:
|
def generate_direct_reply(self, user_input: str, context: str = "") -> str:
|
||||||
"""无需工具时直接调用 OpenAI 生成回复"""
|
"""无需工具时直接调用 OpenAI 生成回复(不涉及 tool 消息,无需修复)"""
|
||||||
self.logger.info("💬 直接生成回复(无需工具)...")
|
self.logger.info("💬 直接生成回复(无需工具)...")
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": "你是一个友好、专业的 AI 助手,请简洁准确地回答用户问题。"},
|
{"role": "system", "content": "你是一个友好、专业的 AI 助手,请简洁准确地回答用户问题。"},
|
||||||
|
|
@ -243,31 +265,63 @@ class LLMEngine:
|
||||||
result = self.provider.generate_reply(messages)
|
result = self.provider.generate_reply(messages)
|
||||||
if result.success and result.content:
|
if result.success and result.content:
|
||||||
return result.content
|
return result.content
|
||||||
# 降级
|
|
||||||
return (
|
return (
|
||||||
f"[{self.cfg.model_name}] 您好!\n"
|
f"您好!关于「{user_input}」,我已收到您的问题。\n"
|
||||||
f"关于「{user_input}」,我已收到您的问题。\n"
|
|
||||||
f"(API 暂时不可用,请检查 API Key 配置)"
|
f"(API 暂时不可用,请检查 API Key 配置)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 消息构造
|
# 消息构造(修复核心)
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_reply_messages_with_block(
|
||||||
|
user_input: str,
|
||||||
|
openai_tool_block: list[dict],
|
||||||
|
) -> list[dict]:
|
||||||
|
"""
|
||||||
|
构造回复生成阶段的完整消息列表
|
||||||
|
|
||||||
|
✅ 正确结构:
|
||||||
|
[
|
||||||
|
{"role": "system", "content": REPLY_SYSTEM_PROMPT},
|
||||||
|
{"role": "user", "content": user_input},
|
||||||
|
{"role": "assistant", "content": null, "tool_calls": [...]}, ← tool_block[0]
|
||||||
|
{"role": "tool", "content": "...", "tool_call_id": "..."}, ← tool_block[1]
|
||||||
|
{"role": "tool", "content": "...", "tool_call_id": "..."}, ← tool_block[2]
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
⚠️ 注意: openai_tool_block 必须整体追加,不能拆分或重排
|
||||||
|
"""
|
||||||
|
from llm.providers.openai_provider import OpenAIProvider
|
||||||
|
messages: list[dict] = [
|
||||||
|
{"role": "system", "content": OpenAIProvider._REPLY_SYSTEM_PROMPT},
|
||||||
|
{"role": "user", "content": user_input},
|
||||||
|
]
|
||||||
|
# 整体追加工具消息块(assistant + tool(s))
|
||||||
|
messages.extend(openai_tool_block)
|
||||||
|
return messages
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_plan_messages(
|
def _build_plan_messages(
|
||||||
user_input: str,
|
user_input: str,
|
||||||
context: str,
|
context: str,
|
||||||
history: list[dict] | None,
|
history: list[dict] | None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""构造规划阶段的消息列表"""
|
"""构造规划阶段的消息列表(不含 tool 消息,无需修复)"""
|
||||||
from llm.providers.openai_provider import OpenAIProvider
|
from llm.providers.openai_provider import OpenAIProvider
|
||||||
messages: list[dict] = [
|
messages: list[dict] = [
|
||||||
{"role": "system", "content": OpenAIProvider._PLANNER_SYSTEM_PROMPT},
|
{"role": "system", "content": OpenAIProvider._PLANNER_SYSTEM_PROMPT},
|
||||||
]
|
]
|
||||||
# 注入结构化历史(优先)或文本摘要
|
|
||||||
if history:
|
if history:
|
||||||
messages.extend(history[-6:]) # 最近 3 轮
|
# 过滤掉 tool 消息,只保留 user/assistant 对话历史
|
||||||
|
clean_history = [
|
||||||
|
m for m in history[-6:]
|
||||||
|
if m.get("role") in ("user", "assistant")
|
||||||
|
and not m.get("tool_calls") # 排除含 tool_calls 的 assistant 消息
|
||||||
|
]
|
||||||
|
messages.extend(clean_history)
|
||||||
elif context and context != "(暂无对话历史)":
|
elif context and context != "(暂无对话历史)":
|
||||||
messages.append({
|
messages.append({
|
||||||
"role": "system",
|
"role": "system",
|
||||||
|
|
@ -277,29 +331,87 @@ class LLMEngine:
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_reply_messages(
|
def _validate_tool_block(openai_tool_block: list[dict]) -> bool:
|
||||||
|
"""
|
||||||
|
验证 openai_tool_block 消息块合规性
|
||||||
|
|
||||||
|
规则:
|
||||||
|
1. 第一条必须是 role=assistant 且含 tool_calls
|
||||||
|
2. 后续每条必须是 role=tool 且含 tool_call_id
|
||||||
|
3. tool 消息数量 == assistant.tool_calls 数量
|
||||||
|
4. 所有 tool_call_id 必须在 assistant.tool_calls[].id 中存在
|
||||||
|
"""
|
||||||
|
if not openai_tool_block:
|
||||||
|
return False
|
||||||
|
|
||||||
|
first = openai_tool_block[0]
|
||||||
|
if first.get("role") != "assistant" or not first.get("tool_calls"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
declared_ids = {tc["id"] for tc in first["tool_calls"]}
|
||||||
|
tool_msgs = openai_tool_block[1:]
|
||||||
|
|
||||||
|
if len(tool_msgs) != len(declared_ids):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for msg in tool_msgs:
|
||||||
|
if msg.get("role") != "tool":
|
||||||
|
return False
|
||||||
|
if msg.get("tool_call_id") not in declared_ids:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _generate_simple_reply(
|
||||||
|
self,
|
||||||
user_input: str,
|
user_input: str,
|
||||||
tool_messages: list[dict],
|
tool_name: str,
|
||||||
) -> list[dict]:
|
tool_output: str,
|
||||||
"""构造回复生成阶段的消息列表(含工具执行结果)"""
|
) -> str:
|
||||||
from llm.providers.openai_provider import OpenAIProvider
|
"""无 tool_call_id 时的降级回复(不走 Function Calling 协议)"""
|
||||||
messages: list[dict] = [
|
messages = [
|
||||||
{"role": "system", "content": OpenAIProvider._REPLY_SYSTEM_PROMPT},
|
{"role": "system",
|
||||||
{"role": "user", "content": user_input},
|
"content": "你是一个友好的 AI 助手,请基于工具执行结果回答用户问题。"},
|
||||||
|
{"role": "user",
|
||||||
|
"content": (
|
||||||
|
f"用户问题: {user_input}\n\n"
|
||||||
|
f"工具 [{tool_name}] 执行结果:\n{tool_output}\n\n"
|
||||||
|
f"请基于以上结果给出清晰的回答。"
|
||||||
|
)},
|
||||||
]
|
]
|
||||||
messages.extend(tool_messages)
|
result = self.provider.generate_reply(messages)
|
||||||
return messages
|
return result.content if result.success else self._fallback_chain_reply(
|
||||||
|
user_input, tool_output
|
||||||
|
)
|
||||||
|
|
||||||
|
def _log_messages_structure(self, messages: list[dict]) -> None:
|
||||||
|
"""调试:打印消息序列结构(不打印完整内容)"""
|
||||||
|
self.logger.debug("📋 消息序列结构:")
|
||||||
|
for i, msg in enumerate(messages):
|
||||||
|
role = msg.get("role", "?")
|
||||||
|
if role == "assistant" and msg.get("tool_calls"):
|
||||||
|
ids = [tc["id"] for tc in msg["tool_calls"]]
|
||||||
|
names = [tc["function"]["name"] for tc in msg["tool_calls"]]
|
||||||
|
self.logger.debug(
|
||||||
|
f" [{i}] {role:10s} tool_calls={names} ids={ids}"
|
||||||
|
)
|
||||||
|
elif role == "tool":
|
||||||
|
self.logger.debug(
|
||||||
|
f" [{i}] {role:10s} tool_call_id={msg.get('tool_call_id')} "
|
||||||
|
f"content={str(msg.get('content',''))[:40]}..."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
content_preview = str(msg.get("content", ""))[:50]
|
||||||
|
self.logger.debug(f" [{i}] {role:10s} {content_preview}...")
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 降级规则引擎
|
# 降级规则引擎
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def _rule_based_plan(self, user_input: str) -> ChainPlan:
|
def _rule_based_plan(self, user_input: str) -> ChainPlan:
|
||||||
"""规则引擎(API 不可用时的降级方案)"""
|
|
||||||
self.logger.info("⚙️ 使用规则引擎规划...")
|
self.logger.info("⚙️ 使用规则引擎规划...")
|
||||||
text = user_input.lower()
|
text = user_input.lower()
|
||||||
|
|
||||||
# 搜索 + 计算
|
|
||||||
if (any(k in text for k in ["搜索", "查询", "查一下"]) and
|
if (any(k in text for k in ["搜索", "查询", "查一下"]) and
|
||||||
any(k in text for k in ["计算", "算", "等于", "结果"])):
|
any(k in text for k in ["计算", "算", "等于", "结果"])):
|
||||||
return ChainPlan(
|
return ChainPlan(
|
||||||
|
|
@ -314,7 +426,6 @@ class LLMEngine:
|
||||||
"进行计算", [1]),
|
"进行计算", [1]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
# 读取文件 + 执行代码
|
|
||||||
if (any(k in text for k in ["读取", "文件", "file"]) and
|
if (any(k in text for k in ["读取", "文件", "file"]) and
|
||||||
any(k in text for k in ["执行", "运行", "run"])):
|
any(k in text for k in ["执行", "运行", "run"])):
|
||||||
fname = re.search(r"[\w\-\.]+\.\w+", user_input)
|
fname = re.search(r"[\w\-\.]+\.\w+", user_input)
|
||||||
|
|
@ -333,7 +444,6 @@ class LLMEngine:
|
||||||
return self._rule_single_step(user_input)
|
return self._rule_single_step(user_input)
|
||||||
|
|
||||||
def _rule_single_step(self, user_input: str) -> ChainPlan:
|
def _rule_single_step(self, user_input: str) -> ChainPlan:
|
||||||
"""单步规则匹配"""
|
|
||||||
text = user_input.lower()
|
text = user_input.lower()
|
||||||
if any(k in text for k in ["计算", "等于", "×", "÷", "+", "-", "*", "/"]):
|
if any(k in text for k in ["计算", "等于", "×", "÷", "+", "-", "*", "/"]):
|
||||||
expr = self._extract_expression(user_input)
|
expr = self._extract_expression(user_input)
|
||||||
|
|
@ -364,7 +474,6 @@ class LLMEngine:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _fallback_chain_reply(user_input: str, chain_summary: str) -> str:
|
def _fallback_chain_reply(user_input: str, chain_summary: str) -> str:
|
||||||
"""API 不可用时的模板回复"""
|
|
||||||
return (
|
return (
|
||||||
f"✅ **任务已完成**\n\n"
|
f"✅ **任务已完成**\n\n"
|
||||||
f"针对您的需求「{user_input}」,执行结果如下:\n\n"
|
f"针对您的需求「{user_input}」,执行结果如下:\n\n"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue