test: AgentLoop + GovernedLLMClient 集成测试
验证 core/agent/ 通过 LLMProvider Protocol 与 adapters/ 端到端协作。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
|||||||
|
"""AgentLoop + GovernedLLMClient 集成测试。
|
||||||
|
|
||||||
|
验证 core/agent/ 通过 LLMProvider Protocol 与 adapters/llm.py 端到端协作:
|
||||||
|
- GovernedLLMClient 满足 LLMProvider Protocol
|
||||||
|
- AgentLoop 通过 GovernedLLMClient 完成 search + submit_answer 两步推理
|
||||||
|
- token 用量正确累加
|
||||||
|
- 遥测数据正确写入 SQLite
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from adapters.breaker import CircuitBreaker
|
||||||
|
from adapters.llm import GovernedLLMClient
|
||||||
|
from adapters.telemetry import SQLiteTelemetryRecorder
|
||||||
|
from core.agent.loop import AgentLoop
|
||||||
|
from core.protocols import LLMProvider
|
||||||
|
|
||||||
|
|
||||||
|
class _StubDispatcher:
|
||||||
|
"""测试用工具调度器,支持 search_tree 和 submit_answer。"""
|
||||||
|
|
||||||
|
async def dispatch(
|
||||||
|
self, tool_name: str, args: dict[str, Any], *, context: dict[str, Any]
|
||||||
|
) -> str:
|
||||||
|
if tool_name == "submit_answer":
|
||||||
|
return "答案已提交"
|
||||||
|
if tool_name == "search_tree":
|
||||||
|
return "搜索结果: L2-3 节点"
|
||||||
|
raise ValueError(f"未知工具: {tool_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def _streaming_result(content: str, thinking: str = "") -> tuple:
|
||||||
|
"""构造 _call_streaming 的返回值。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
content: 模型输出的文本内容。
|
||||||
|
thinking: 模型思考过程文本。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
(content, thinking, ttft_ms, max_inter_token_ms, usage_dict) 五元组。
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
content,
|
||||||
|
thinking,
|
||||||
|
50.0, # ttft_ms
|
||||||
|
10.0, # max_inter_token_ms
|
||||||
|
{"prompt_tokens": 10, "completion_tokens": 5},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_agent_loop_with_governed_client(tmp_path: Path) -> None:
|
||||||
|
"""AgentLoop 通过 GovernedLLMClient 完成搜索+提交。"""
|
||||||
|
telemetry = SQLiteTelemetryRecorder(db_path=tmp_path / "telemetry.db")
|
||||||
|
client = GovernedLLMClient(
|
||||||
|
model="test-model",
|
||||||
|
base_url="https://api.test.com/v1",
|
||||||
|
api_key="sk-test",
|
||||||
|
provider="deepseek",
|
||||||
|
thinking=True,
|
||||||
|
breaker=CircuitBreaker(fail_threshold=5, cooldown_s=60.0),
|
||||||
|
cache=None,
|
||||||
|
telemetry=telemetry,
|
||||||
|
timeout_s=30.0,
|
||||||
|
ttft_timeout_s=10.0,
|
||||||
|
inter_token_timeout_s=5.0,
|
||||||
|
max_retries=1,
|
||||||
|
retry_base_delay_s=0.01,
|
||||||
|
retry_max_delay_s=0.05,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证 GovernedLLMClient 满足 LLMProvider Protocol
|
||||||
|
assert isinstance(client, LLMProvider)
|
||||||
|
|
||||||
|
call_count = 0
|
||||||
|
responses = [
|
||||||
|
_streaming_result(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"reflect": {},
|
||||||
|
"plan": {},
|
||||||
|
"action": {"tool": "search_tree", "args": {"query": "什么是AI"}},
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
thinking="让我思考一下",
|
||||||
|
),
|
||||||
|
_streaming_result(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"reflect": {},
|
||||||
|
"plan": {},
|
||||||
|
"action": {"tool": "submit_answer", "args": {"answer": "AI是..."}},
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
async def mock_call_streaming(messages: list[dict[str, Any]]) -> tuple:
|
||||||
|
nonlocal call_count
|
||||||
|
result = responses[call_count]
|
||||||
|
call_count += 1
|
||||||
|
return result
|
||||||
|
|
||||||
|
loop = AgentLoop(llm=client, max_steps=10)
|
||||||
|
|
||||||
|
with patch.object(client, "_call_streaming", side_effect=mock_call_streaming):
|
||||||
|
result = await loop.run(
|
||||||
|
system_prompt="你是一个搜索助手",
|
||||||
|
user_prompt="什么是人工智能?",
|
||||||
|
tool_dispatcher=_StubDispatcher(),
|
||||||
|
session_id="test-session",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.stop_reason == "finished"
|
||||||
|
assert result.result == {"answer": "AI是..."}
|
||||||
|
assert result.steps_used == 2
|
||||||
|
assert result.steps[0].thought == "让我思考一下"
|
||||||
|
assert result.steps[0].tool_call["tool"] == "search_tree"
|
||||||
|
assert result.steps[1].tool_call["tool"] == "submit_answer"
|
||||||
|
assert result.token_usage["prompt_tokens"] == 20 # 10 * 2
|
||||||
|
assert result.token_usage["completion_tokens"] == 10 # 5 * 2
|
||||||
Reference in New Issue
Block a user