Compare commits

...

5 Commits

Author SHA1 Message Date
a2a7fd46c3 chore: 版本号更新到v3.0.6 2026-04-15 09:52:31 +08:00
baf5913bfb fix: SiliconFlow平台Function Calling第二轮调用兼容
问题:SiliconFlow平台不支持标准tool消息类型,第二轮调用返回参数无效

修复:将tool消息转换为user消息格式
- 收集所有tool消息的内容
- 合并为一个用户消息发送给模型
- 添加明确的提示让模型直接根据结果回答

版本: v3.0.6
2026-04-15 09:52:19 +08:00
ae08e01e55 fix: Kimi模型伪工具调用格式过滤
修复Kimi-K2.5模型在第二轮调用时输出伪工具调用格式的问题:
- 添加系统提示告诉模型直接根据工具结果回答
- 过滤 <|tool_calls_section_begin|> 等内部格式标记
- 清理多余空行

版本: v3.0.1
2026-04-15 09:45:08 +08:00
9048d94e33 fix: 添加详细日志诊断工具调用消息格式 2026-04-15 02:25:05 +08:00
291de733a4 fix: chat_with_tool_results不重复添加tool结果,修正消息格式 2026-04-15 01:03:10 +08:00
2 changed files with 75 additions and 15 deletions

View File

@@ -33,7 +33,7 @@ logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 创建应用
app = FastAPI(title="AI对话系统 v2.0", version="2.0.0")
app = FastAPI(title="AI对话系统 v3.0", version="3.0.6")
# 静态文件和模板
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -1121,7 +1121,6 @@ async def websocket_endpoint(websocket: WebSocket, user_id: str):
messages=history_with_tools,
provider_config=agent_config['provider'],
agent_config=agent_config['agent'],
tool_results=tool_results,
enable_thinking=enable_thinking
)

View File

@@ -514,17 +514,18 @@ class LLMService:
messages: List[Dict],
provider_config: dict,
agent_config: dict,
tool_results: List[Dict],
enable_thinking: bool = True
) -> Tuple[str, Optional[str]]:
"""
第二阶段调用:将工具执行结果返回给LLM
第二阶段调用:使用包含工具调用和结果的完整消息历史
注意SiliconFlow 等平台不支持标准 tool 消息类型,
需要将工具结果转换为普通用户消息格式。
Args:
messages: 对话历史(包含工具调用和结果)
messages: 已包含assistant tool_calls和tool结果的完整消息历史
provider_config: LLM Provider配置
agent_config: Agent配置
tool_results: 工具执行结果 [{"tool_call_id": "xxx", "content": "..."}]
Returns:
Tuple[str, Optional[str]]: (回复内容, 思考过程)
@@ -535,15 +536,44 @@ class LLMService:
max_tokens = provider_config.get('max_tokens', 4096)
temperature = agent_config.get('temperature_override') or provider_config.get('temperature', 0.7)
# 将工具结果添加到消息历史
final_messages = messages.copy()
for result in tool_results:
final_messages.append({
"role": "tool",
"tool_call_id": result['tool_call_id'],
"content": result['content']
# 转换消息格式:将 tool 相关消息转为普通消息格式
# 因为很多 API 平台(如 SiliconFlow不支持 tool 消息类型
converted_messages = []
tool_results_content = []
for msg in messages:
role = msg.get('role')
if role == 'system':
converted_messages.append(msg)
elif role == 'user':
converted_messages.append(msg)
elif role == 'assistant':
# 如果有 tool_calls跳过这个消息不发送给不支持的平台
if msg.get('tool_calls'):
# 记录工具调用信息(可选)
pass
else:
converted_messages.append(msg)
elif role == 'tool':
# 收集工具结果,后面合并成一个用户消息
tool_results_content.append(msg.get('content', ''))
# 如果有工具结果,添加为一个特殊的用户消息
if tool_results_content:
combined_results = "\n\n".join([
f"【搜索结果 {i+1}\n{result}"
for i, result in enumerate(tool_results_content)
])
# 添加工具结果作为用户消息
converted_messages.append({
"role": "user",
"content": f"以下是搜索工具返回的结果:\n\n{combined_results}\n\n请根据以上搜索结果回答我之前的问题,不要再说\"让我搜索一下\",直接给出回答。如果搜索结果不足以回答,请说明。"
})
final_messages = converted_messages
# 调用LLM生成最终回复
url = f"{api_base.rstrip('/')}/chat/completions"
headers = {
@@ -557,19 +587,50 @@ class LLMService:
"max_tokens": max_tokens
}
logger.info(f"工具结果返回LLM: url={url}, model={model}")
logger.info(f"工具结果返回LLM: url={url}, model={model}, 消息数={len(final_messages)}")
# 打印消息内容(调试)
for i, msg in enumerate(final_messages):
role = msg.get('role')
content_preview = str(msg.get('content', ''))[:100] if msg.get('content') else 'None'
if role == 'tool':
logger.info(f"消息[{i}] role={role}, tool_call_id={msg.get('tool_call_id')}, content长度={len(msg.get('content',''))}")
elif role == 'assistant' and msg.get('tool_calls'):
logger.info(f"消息[{i}] role={role}, tool_calls={len(msg['tool_calls'])}")
else:
logger.info(f"消息[{i}] role={role}, content={content_preview}...")
try:
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(url, headers=headers, json=payload)
if response.status_code != 200:
logger.error(f"API返回错误: status={response.status_code}")
logger.error(f"API返回错误: status={response.status_code}, body={response.text[:500]}")
response.raise_for_status()
data = response.json()
content = data['choices'][0]['message']['content']
# 过滤掉伪工具调用格式某些模型如Kimi会输出这种内部格式
# 模式:<|tool_calls_section_begin|>...<|tool_calls_section_end|>
import re
tool_pattern = r'<\|tool_calls_section_begin\|>.*?<\|tool_calls_section_end\|>'
content = re.sub(tool_pattern, '', content, flags=re.DOTALL)
# 也过滤单个 tool_call 格式
tool_call_pattern = r'<\|tool_call_begin\|>.*?<\|tool_call_end\|>'
content = re.sub(tool_call_pattern, '', content, flags=re.DOTALL)
# 清理可能残留的格式标记
content = content.replace('<|tool_calls_section_begin|>', '')
content = content.replace('<|tool_calls_section_end|>', '')
content = content.replace('<|tool_call_begin|>', '')
content = content.replace('<|tool_call_end|>', '')
content = content.replace('<|tool_call_argument_begin|>', '')
content = content.replace('<|tool_call_argument_end|>', '')
# 清理多余空行
content = re.sub(r'\n{3,}', '\n\n', content).strip()
return content, None
except Exception as e: