feat: 增强工具调用代理功能,支持多工具调用和消息历史转换

主要改进:
- 新增 convert_tool_calls_to_content 函数,将消息历史中的 tool_calls 转换为 LLM 可理解的 XML 格式
- 修复 response_parser 支持同时解析多个 tool_calls
- 优化响应解析逻辑,支持 content 和 tool_calls 同时存在
- 添加完整的测试覆盖,包括多工具调用、消息转换和混合响应

技术细节:
- services.py: 实现工具调用历史到 content 的转换
- response_parser.py: 使用非贪婪匹配支持多个 tool_calls 解析
- main.py: 集成消息转换功能,确保消息历史正确传递给 LLM

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Vertex-AI-Step-Builder
2025-12-31 13:33:25 +00:00
parent f7508d915b
commit 5c2904e010
6 changed files with 624 additions and 31 deletions

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""
测试 tool_calls 到 content 的转换功能
"""
import sys
import os
# 添加项目路径到 sys.path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app.services import convert_tool_calls_to_content
from app.models import ChatMessage
def test_convert_tool_calls_to_content():
"""测试工具调用转换功能"""
# 测试用例 1: 带有 tool_calls 的 assistant 消息
print("=" * 60)
print("测试用例 1: 带有 tool_calls 的 assistant 消息")
print("=" * 60)
messages = [
ChatMessage(
role="user",
content="帮我查询一下天气"
),
ChatMessage(
role="assistant",
tool_calls=[
{
"id": "call_123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"location": "北京", "unit": "celsius"}'
}
}
]
),
ChatMessage(
role="user",
content="那上海呢?"
)
]
print("\n原始消息:")
for i, msg in enumerate(messages):
print(f" 消息 {i+1}:")
print(f" 角色: {msg.role}")
if msg.content:
print(f" 内容: {msg.content}")
if msg.tool_calls:
print(f" 工具调用: {len(msg.tool_calls)}")
# 转换
converted = convert_tool_calls_to_content(messages)
print("\n转换后的消息:")
for i, msg in enumerate(converted):
print(f" 消息 {i+1}:")
print(f" 角色: {msg.role}")
if msg.content:
print(f" 内容: {msg.content[:100]}...") # 只显示前100个字符
# 验证第二个消息是否被正确转换
assert converted[1].role == "assistant"
assert "<invoke>" in converted[1].content
assert "get_weather" in converted[1].content
assert "北京" in converted[1].content
assert converted[1].tool_calls is None # tool_calls 应该被移除
print("\n✓ 测试用例 1 通过!")
# 测试用例 2: 带有 content 和 tool_calls 的 assistant 消息
print("\n" + "=" * 60)
print("测试用例 2: 带有 content 和 tool_calls 的 assistant 消息")
print("=" * 60)
messages2 = [
ChatMessage(
role="assistant",
content="好的,让我帮你查询天气。",
tool_calls=[
{
"id": "call_456",
"type": "function",
"function": {
"name": "search",
"arguments": '{"query": "今天天气"}'
}
}
]
)
]
print("\n原始消息:")
print(f" 角色: {messages2[0].role}")
print(f" 内容: {messages2[0].content}")
print(f" 工具调用: {messages2[0].tool_calls}")
converted2 = convert_tool_calls_to_content(messages2)
print("\n转换后的消息:")
print(f" 角色: {converted2[0].role}")
print(f" 内容: {converted2[0].content}")
# 验证
assert "好的,让我帮你查询天气。" in converted2[0].content
assert "<invoke>" in converted2[0].content
assert "search" in converted2[0].content
print("\n✓ 测试用例 2 通过!")
# 测试用例 3: 多个 tool_calls
print("\n" + "=" * 60)
print("测试用例 3: 多个 tool_calls")
print("=" * 60)
messages3 = [
ChatMessage(
role="assistant",
tool_calls=[
{
"id": "call_1",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"location": "北京"}'
}
},
{
"id": "call_2",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"location": "上海"}'
}
}
]
)
]
print("\n原始消息:")
print(f" 角色: {messages3[0].role}")
print(f" 工具调用数量: {len(messages3[0].tool_calls)}")
converted3 = convert_tool_calls_to_content(messages3)
print("\n转换后的消息:")
print(f" 内容: {converted3[0].content}")
# 验证两个工具调用都被转换
assert converted3[0].content.count("<invoke>") == 2
assert "北京" in converted3[0].content
assert "上海" in converted3[0].content
print("\n✓ 测试用例 3 通过!")
# 测试用例 4: 没有 tool_calls 的消息(应该保持不变)
print("\n" + "=" * 60)
print("测试用例 4: 没有 tool_calls 的消息")
print("=" * 60)
messages4 = [
ChatMessage(role="user", content="你好"),
ChatMessage(role="assistant", content="你好,有什么可以帮助你的吗?"),
ChatMessage(role="user", content="再见")
]
print("\n原始消息:")
for i, msg in enumerate(messages4):
print(f" 消息 {i+1}: {msg.role} - {msg.content}")
converted4 = convert_tool_calls_to_content(messages4)
print("\n转换后的消息:")
for i, msg in enumerate(converted4):
print(f" 消息 {i+1}: {msg.role} - {msg.content}")
# 验证消息保持不变
assert len(converted4) == len(messages4)
assert converted4[0].content == "你好"
assert converted4[1].content == "你好,有什么可以帮助你的吗?"
assert converted4[2].content == "再见"
print("\n✓ 测试用例 4 通过!")
print("\n" + "=" * 60)
print("所有测试用例通过! ✓")
print("=" * 60)
if __name__ == "__main__":
test_convert_tool_calls_to_content()