This commit is contained in:
sontolau 2026-03-09 13:40:27 +08:00
parent 169666443f
commit 3640f3c216
2 changed files with 367 additions and 232 deletions

View File

@ -1,9 +1,11 @@
"""
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
from dataclasses import dataclass, field
@ -39,23 +41,31 @@ class AgentClient:
"""
Agent 客户端OpenAI Function Calling + Multi-Step Tool Chain
执行流程:
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 消息序列规范:
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": "assistant", "tool_calls": [{web_search}, {calculator}]}
{"role": "tool", "content": "web_search 结果", "tool_call_id": "call_1"}
{"role": "tool", "content": "calculator 结果", "tool_call_id": "call_2"}
generate_reply() 最终自然语言回复
{"role": "assistant", "content": null,
"tool_calls": [{"id": "call_001", ...web_search...},
{"id": "call_002", ...calculator...}]}
{"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__(
@ -68,7 +78,6 @@ class AgentClient:
self.mcp_server = mcp_server
self.memory = memory
self.logger = get_logger("CLIENT")
# OpenAI 格式的结构化对话历史(跨轮次保持上下文)
self._openai_history: list[dict] = []
self.logger.info("💻 Agent Client 初始化完成OpenAI Function Calling 模式)")
@ -83,12 +92,11 @@ class AgentClient:
self.logger.info(f"📨 收到用户输入: {user_input}")
self.logger.info(sep)
# Step 1: 记录用户消息
self.memory.add_user_message(user_input)
context = self.memory.get_context_summary()
# Step 2: LLM 规划工具调用链
self.logger.info("🗺 Step 2 [LLM] 规划工具调用链...")
# Step 1: LLM 规划工具调用链
self.logger.info("🗺 [LLM] 规划工具调用链...")
tool_schemas = self.mcp_server.get_tool_schemas()
plan: ChainPlan = self.llm.plan_tool_chain(
user_input=user_input,
@ -101,11 +109,13 @@ class AgentClient:
if not plan.steps:
return self._handle_direct_reply(user_input, context)
# Step 3~4: 执行工具调用链,构造 OpenAI 消息序列
chain_result, tool_messages = self._execute_chain(plan, user_input)
# Step 2: 执行工具调用链,构造合规的 OpenAI 消息序列
chain_result, openai_tool_block = self._execute_chain(plan, user_input)
# Step 5: 调用 OpenAI 整合结果,生成最终回复
return self._generate_response(user_input, chain_result, tool_messages, context)
# Step 3: 调用 OpenAI 整合结果,生成最终回复
return self._generate_response(
user_input, chain_result, openai_tool_block, context
)
# ════════════════════════════════════════════════════════════
# 串行执行引擎
@ -117,12 +127,20 @@ class AgentClient:
user_input: str,
) -> tuple[ChainResult, list[dict]]:
"""
串行执行工具调用链同步构造 OpenAI 消息序列
串行执行工具调用链构造符合 OpenAI 协议的消息块
Returns:
(ChainResult, tool_messages)
tool_messages OpenAI 格式的工具调用消息列表
用于后续 generate_reply() 调用
chain_result: 执行结果汇总
openai_tool_block: 合规的 OpenAI 消息块结构为:
[
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(
f"\n{'' * 60}\n"
@ -132,21 +150,26 @@ class AgentClient:
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] = []
tool_result_msgs: list[dict] = []
chain_context: dict[str, str] = {}
tool_messages: list[dict] = []
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:
# 检查前置依赖
call_id = step_call_ids[step.step_id]
if self._has_failed_dependency(step, failed_step):
self.logger.warning(
f"⏭ Step {step.step_id} [{step.tool_name}] 跳过"
@ -159,25 +182,22 @@ class AgentClient:
output="",
error=f"跳过:依赖步骤 {failed_step} 失败",
))
# 向 OpenAI 消息序列写入失败占位
tool_messages.append({
# ⚠️ 即使跳过,也必须补一条 tool 消息,保持 id 完全匹配
tool_result_msgs.append({
"role": "tool",
"content": f"步骤跳过:依赖步骤 {failed_step} 执行失败",
"tool_call_id": assistant_tool_calls[step.step_id - 1]["id"],
"tool_call_id": call_id,
})
continue
# 执行单步
result, tool_call_id = self._execute_single_step(
step, chain_context, assistant_tool_calls
)
result = self._execute_single_step(step, chain_context, call_id)
step_results.append(result)
# 追加 tool 消息到 OpenAI 序列
tool_messages.append({
# 追加 tool 结果消息tool_call_id 与 assistant 声明完全一致)
tool_result_msgs.append({
"role": "tool",
"content": result.output if result.success else f"执行失败: {result.error}",
"tool_call_id": tool_call_id,
"tool_call_id": call_id,
})
if result.success:
@ -186,6 +206,22 @@ class AgentClient:
else:
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
chain_result = ChainResult(
goal=plan.goal,
@ -200,35 +236,26 @@ class AgentClient:
f" 完成: {chain_result.completed_steps}/{chain_result.total_steps}\n"
f"{'' * 60}"
)
return chain_result, tool_messages
return chain_result, openai_tool_block
def _execute_single_step(
self,
step: ToolStep,
chain_context: dict[str, str],
assistant_tool_calls: list[dict],
) -> tuple[StepResult, str]:
"""
执行单个步骤返回 (StepResult, tool_call_id)
Returns:
StepResult: 步骤执行结果
tool_call_id: 对应的 OpenAI tool_call_id用于消息序列关联
"""
# 注入前步上下文(占位符替换)
resolved_step = step.inject_context(chain_context)
tool_call_id = assistant_tool_calls[step.step_id - 1]["id"]
step: ToolStep,
chain_context: dict[str, str],
call_id: str,
) -> StepResult:
"""执行单个步骤,返回 StepResult"""
resolved_step = step.inject_context(chain_context)
self.logger.info(
f"\n ▶ Step {step.step_id} 执行中\n"
f" 工具: [{resolved_step.tool_name}]\n"
f" 说明: {resolved_step.description}\n"
f" 参数: {resolved_step.arguments}\n"
f" call_id: {tool_call_id}"
f" 工具 : [{resolved_step.tool_name}]\n"
f" 说明 : {resolved_step.description}\n"
f" 参数 : {resolved_step.arguments}\n"
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)
if mcp_response.success:
@ -239,7 +266,7 @@ class AgentClient:
tool_name=step.tool_name,
success=True,
output=output,
), tool_call_id
)
else:
error_msg = mcp_response.error.get("message", "未知错误")
self.logger.error(f" ❌ Step {step.step_id} 失败: {error_msg}")
@ -249,7 +276,7 @@ class AgentClient:
success=False,
output="",
error=error_msg,
), tool_call_id
)
# ════════════════════════════════════════════════════════════
# 回复生成
@ -257,57 +284,46 @@ class AgentClient:
def _generate_response(
self,
user_input: str,
chain_result: ChainResult,
tool_messages: list[dict],
context: str,
user_input: str,
chain_result: ChainResult,
openai_tool_block: list[dict],
context: str,
) -> AgentResponse:
"""调用 OpenAI 整合工具结果,生成最终 AgentResponse"""
self.logger.info("✍️ Step 5 [LLM] 调用 OpenAI 生成最终回复...")
self.logger.info("✍️ [LLM] 调用 OpenAI 生成最终回复...")
chain_summary = self._build_chain_summary(chain_result)
# 单步走简洁路径
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(
user_input=user_input,
chain_summary=chain_summary,
context=context,
tool_messages=tool_messages,
)
# 将合规的消息块传给 LLMEngine
final_reply = self.llm.generate_chain_reply(
user_input=user_input,
chain_summary=chain_summary,
context=context,
openai_tool_block=openai_tool_block, # ← 传递完整合规消息块
)
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": "assistant", "content": final_reply})
# 保留最近 10 轮
if len(self._openai_history) > 20:
self._openai_history = self._openai_history[-20:]
# 写入 Memory
if chain_result.total_steps > 1:
self.memory.add_chain_result(chain_result)
else:
self.memory.add_assistant_message(final_reply)
self.logger.info("🎉 [CLIENT] 流程完成,回复已返回")
self.logger.info("🎉 流程完成,回复已返回")
return AgentResponse(
user_input=user_input,
final_reply=final_reply,
chain_result=chain_result,
tool_used=chain_result.step_results[0].tool_name if chain_result.total_steps == 1 else None,
tool_output=chain_result.step_results[0].output if chain_result.total_steps == 1 else None,
tool_used=(chain_result.step_results[0].tool_name
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,
)
@ -321,42 +337,52 @@ class AgentClient:
return AgentResponse(user_input=user_input, final_reply=reply)
# ════════════════════════════════════════════════════════════
# 工具方法
# 消息构造工具方法
# ════════════════════════════════════════════════════════════
@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",
"type": "function",
"function": {
"name": "calculator",
"arguments": '{"expression": "1+2"}'
正确格式:
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123", 与后续 tool 消息的 tool_call_id 完全一致
"type": "function",
"function": {
"name": "calculator",
"arguments": "{\"expression\": \"1+2\"}" 必须是 JSON 字符串
}
}
}
]
]
}
"""
import json
tool_calls = []
for step in plan.steps:
tool_calls.append({
"id": f"call_{uuid.uuid4().hex[:8]}",
"id": step_call_ids[step.step_id],
"type": "function",
"function": {
"name": step.tool_name,
"arguments": json.dumps(step.arguments, ensure_ascii=False),
},
})
return tool_calls
return {
"role": "assistant",
"content": None,
"tool_calls": tool_calls,
}
@staticmethod
def _build_chain_summary(chain_result: ChainResult) -> str:
"""将调用链结果格式化为 LLM 可读的摘要"""
"""将调用链结果格式化为 LLM 可读的摘要(降级时使用)"""
lines = []
for r in chain_result.step_results:
if r.success:

View File

@ -1,7 +1,7 @@
"""
llm/llm_engine.py
LLM 引擎通过 Provider 接口调用真实 OpenAI API
支持 Function Calling 多步骤规划 + 工具结果整合回复
修复generate_chain_reply / generate_final_reply 消息序列构造
确保 tool 消息始终紧跟在含 tool_calls assistant 消息之后
"""
import re
@ -35,25 +35,26 @@ class ToolDecision:
class LLMEngine:
"""
LLM 推理引擎Provider 模式
LLM 推理引擎
核心流程:
1. plan_tool_chain()
构造 OpenAI 格式消息 + tools
Provider.plan_with_tools()
解析 tool_calls ChainPlan
修复后的消息序列规范:
2. generate_chain_reply()
构造含工具结果的完整消息历史
Provider.generate_reply()
最终自然语言回复
generate_chain_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() 最终回复
降级策略:
API 调用失败 fallback_to_rules=true
自动切换到规则引擎保证系统可用性
绝对不能在 tool 消息前插入任何其他消息尤其是 system/user
"""
# 规则引擎关键词(降级时使用)
_MULTI_STEP_KEYWORDS = [
"然后", "接着", "", "并且", "同时", "之后",
"先.*再", "首先.*然后", "搜索.*计算", "读取.*执行",
@ -75,13 +76,12 @@ class LLMEngine:
self.logger.info(f" fallback_rules = {settings.agent.fallback_to_rules}")
def reconfigure(self, cfg: LLMConfig) -> None:
"""热更新配置并重建 Provider"""
self.cfg = cfg
self.provider = create_provider(cfg)
self.logger.info(f"🔄 LLM 配置已更新: model={cfg.model_name}")
# ════════════════════════════════════════════════════════════
# 核心接口
# 工具规划
# ════════════════════════════════════════════════════════════
def plan_tool_chain(
@ -91,35 +91,16 @@ class LLMEngine:
context: str = "",
history: list[dict] | None = None,
) -> ChainPlan:
"""
使用 OpenAI Function Calling 规划工具调用链
消息构造策略:
system 规划器系统提示
history 历史对话可选
user 当前用户输入
Args:
user_input: 用户输入文本
tool_schemas: 可用工具列表
context: 对话历史摘要文本格式用于无 history
history: 结构化对话历史OpenAI 消息格式优先使用
Returns:
ChainPlan 实例
"""
"""使用 OpenAI Function Calling 规划工具调用链"""
self.logger.info(f"🗺 规划工具调用链: {user_input[:60]}...")
# 构造消息列表
messages = self._build_plan_messages(user_input, context, history)
if self.cfg.function_calling:
# ── 真实 OpenAI Function Calling ──────────────────
result = self.provider.plan_with_tools(messages, tool_schemas)
if result.success and result.plan is not None:
plan = result.plan
# 补充 goal 字段
if not plan.goal:
plan.goal = user_input
self.logger.info(f"📋 OpenAI 规划完成: {plan.step_count}")
@ -130,15 +111,12 @@ class LLMEngine:
)
return plan
# API 失败处理
self.logger.warning(f"⚠️ OpenAI 规划失败: {result.error}")
if settings.agent.fallback_to_rules:
self.logger.info("🔄 降级到规则引擎...")
return self._rule_based_plan(user_input)
return ChainPlan(goal=user_input, steps=[])
else:
# function_calling=false 时直接使用规则引擎
self.logger.info("⚙️ function_calling=false使用规则引擎")
return self._rule_based_plan(user_input)
@ -148,7 +126,6 @@ class LLMEngine:
tool_schemas: list[ToolSchema],
context: str = "",
) -> ToolDecision:
"""单步工具决策(代理到 plan_tool_chain"""
plan = self.plan_tool_chain(user_input, tool_schemas, context)
if not plan.steps:
return ToolDecision(need_tool=False, reasoning="无需工具,直接回复")
@ -160,114 +137,191 @@ class LLMEngine:
reasoning=first.description,
)
# ════════════════════════════════════════════════════════════
# 回复生成(核心修复区域)
# ════════════════════════════════════════════════════════════
def generate_chain_reply(
self,
user_input: str,
chain_summary: str,
context: str = "",
tool_messages: list[dict] | None = None,
user_input: str,
chain_summary: str,
context: str = "",
openai_tool_block: list[dict] | None = None,
) -> str:
"""
整合多步骤执行结果调用 OpenAI 生成最终自然语言回复
整合工具执行结果调用 OpenAI 生成最终自然语言回复
消息构造含工具执行结果:
system 回复生成系统提示
user 原始用户输入
assistant 工具调用决策tool_calls
tool 工具执行结果
...多轮工具调用
修复后消息序列:
system 回复生成提示
user 原始用户输入
openai_tool_block 直接追加已包含 assistant+tool 消息
assistant(tool_calls=[...])
tool(tool_call_id=..., content=...)
tool(tool_call_id=..., content=...)
...
修复前的错误导致 400:
system user tool tool tool 前缺少 assistant(tool_calls)
Args:
user_input: 原始用户输入
chain_summary: 步骤摘要API 失败时的降级内容
context: 对话历史
tool_messages: 完整的工具调用消息序列OpenAI 格式
Returns:
最终回复字符串
user_input: 原始用户输入
chain_summary: 步骤摘要API 失败时的降级内容
context: 对话历史仅规划阶段使用回复阶段不注入
openai_tool_block: AgentClient 构造的合规消息块
格式: [assistant(tool_calls), tool, tool, ...]
"""
self.logger.info("✍️ 生成最终回复...")
self.logger.info("✍️ 生成最终回复(工具调用链模式)...")
if tool_messages:
# 构造含工具结果的完整消息历史
messages = self._build_reply_messages(user_input, tool_messages)
result = self.provider.generate_reply(messages)
if not openai_tool_block:
self.logger.warning("⚠️ openai_tool_block 为空,降级到摘要模板")
return self._fallback_chain_reply(user_input, chain_summary)
if result.success and result.content:
self.logger.info(
f"✅ OpenAI 回复生成成功 ({len(result.content)} chars)"
)
return result.content
# 验证消息块合规性
if not self._validate_tool_block(openai_tool_block):
self.logger.warning("⚠️ 消息块验证失败,降级到摘要模板")
return self._fallback_chain_reply(user_input, chain_summary)
self.logger.warning(f"⚠️ OpenAI 回复生成失败: {result.error}")
# ✅ 正确的消息序列构造
messages = self._build_reply_messages_with_block(user_input, openai_tool_block)
# 降级:使用模板回复
self.logger.debug(f"📤 发送回复请求,消息数: {len(messages)}")
self._log_messages_structure(messages)
result = self.provider.generate_reply(messages)
if result.success and result.content:
self.logger.info(
f"✅ OpenAI 回复生成成功 ({len(result.content)} chars)"
)
return result.content
self.logger.warning(f"⚠️ OpenAI 回复生成失败: {result.error}")
return self._fallback_chain_reply(user_input, chain_summary)
def generate_final_reply(
self,
user_input: str,
tool_name: str,
tool_output: str,
context: str = "",
user_input: str,
tool_name: str,
tool_output: str,
context: str = "",
tool_call_id: 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}]...")
# 构造单步工具消息
tool_messages = []
if tool_call_id:
tool_messages = [
{
"role": "tool",
"content": tool_output,
"tool_call_id": tool_call_id,
}
]
if not tool_call_id:
# 无 tool_call_id 时降级到直接回复模式
self.logger.warning("⚠️ tool_call_id 为空,使用直接回复模式")
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",
"content": tool_output,
"tool_call_id": tool_call_id,
},
]
return self.generate_chain_reply(
user_input=user_input,
chain_summary=tool_output,
context=context,
tool_messages=tool_messages,
openai_tool_block=single_tool_block,
)
def generate_direct_reply(self, user_input: str, context: str = "") -> str:
"""无需工具时直接调用 OpenAI 生成回复"""
"""无需工具时直接调用 OpenAI 生成回复(不涉及 tool 消息,无需修复)"""
self.logger.info("💬 直接生成回复(无需工具)...")
messages = [
{"role": "system", "content": "你是一个友好、专业的 AI 助手,请简洁准确地回答用户问题。"},
{"role": "user", "content": user_input},
{"role": "system", "content": "你是一个友好、专业的 AI 助手,请简洁准确地回答用户问题。"},
{"role": "user", "content": user_input},
]
result = self.provider.generate_reply(messages)
if result.success and result.content:
return result.content
# 降级
return (
f"[{self.cfg.model_name}] 您好!\n"
f"关于「{user_input}」,我已收到您的问题。\n"
f"您好!关于「{user_input}」,我已收到您的问题。\n"
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
def _build_plan_messages(
user_input: str,
context: str,
history: list[dict] | None,
) -> list[dict]:
"""构造规划阶段的消息列表"""
"""构造规划阶段的消息列表(不含 tool 消息,无需修复)"""
from llm.providers.openai_provider import OpenAIProvider
messages: list[dict] = [
{"role": "system", "content": OpenAIProvider._PLANNER_SYSTEM_PROMPT},
]
# 注入结构化历史(优先)或文本摘要
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 != "(暂无对话历史)":
messages.append({
"role": "system",
@ -277,29 +331,87 @@ class LLMEngine:
return messages
@staticmethod
def _build_reply_messages(
user_input: str,
tool_messages: list[dict],
) -> list[dict]:
"""构造回复生成阶段的消息列表(含工具执行结果)"""
from llm.providers.openai_provider import OpenAIProvider
messages: list[dict] = [
{"role": "system", "content": OpenAIProvider._REPLY_SYSTEM_PROMPT},
{"role": "user", "content": user_input},
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,
tool_name: str,
tool_output: str,
) -> str:
"""无 tool_call_id 时的降级回复(不走 Function Calling 协议)"""
messages = [
{"role": "system",
"content": "你是一个友好的 AI 助手,请基于工具执行结果回答用户问题。"},
{"role": "user",
"content": (
f"用户问题: {user_input}\n\n"
f"工具 [{tool_name}] 执行结果:\n{tool_output}\n\n"
f"请基于以上结果给出清晰的回答。"
)},
]
messages.extend(tool_messages)
return messages
result = self.provider.generate_reply(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:
"""规则引擎API 不可用时的降级方案)"""
self.logger.info("⚙️ 使用规则引擎规划...")
text = user_input.lower()
# 搜索 + 计算
if (any(k in text for k in ["搜索", "查询", "查一下"]) and
any(k in text for k in ["计算", "", "等于", "结果"])):
return ChainPlan(
@ -314,7 +426,6 @@ class LLMEngine:
"进行计算", [1]),
],
)
# 读取文件 + 执行代码
if (any(k in text for k in ["读取", "文件", "file"]) and
any(k in text for k in ["执行", "运行", "run"])):
fname = re.search(r"[\w\-\.]+\.\w+", user_input)
@ -333,7 +444,6 @@ class LLMEngine:
return self._rule_single_step(user_input)
def _rule_single_step(self, user_input: str) -> ChainPlan:
"""单步规则匹配"""
text = user_input.lower()
if any(k in text for k in ["计算", "等于", "×", "÷", "+", "-", "*", "/"]):
expr = self._extract_expression(user_input)
@ -364,7 +474,6 @@ class LLMEngine:
@staticmethod
def _fallback_chain_reply(user_input: str, chain_summary: str) -> str:
"""API 不可用时的模板回复"""
return (
f"✅ **任务已完成**\n\n"
f"针对您的需求「{user_input}」,执行结果如下:\n\n"