引言
传统软件测试的基石是确定性:相同输入产生相同输出。LLM Agent 打破了这个前提,让 CI/CD 测试面临根本性挑战。2026年,随着 Agent 评估框架的成熟,一套可落地的 CI/CD 测试方法论终于成型。本文将带你构建完整的 Agent 测试金字塔。
一、Agent 测试金字塔
┌─────────┐
│ E2E │ ← 端到端场景测试(5-10个)
┌┴─────────┴┐
│ Integration │ ← 多 Agent 协作测试(20-50个)
┌┴──────────────┴┐
│ Evaluation │ ← LLM 评判测试(50-100个)
┌┴──────────────────┴┐
│ Component │ ← 工具/Prompt 测试(200+)
┌┴──────────────────────┴┐
│ Unit Test │ ← 纯函数测试(500+)
└───────────────────────────┘
二、第一层:单元测试(确定性层)
单元测试只测试不涉及 LLM 的部分:数据处理、工具执行的解析逻辑、Prompt 模板渲染。
import pytest
class TestPromptTemplate:
"""Prompt 模板渲染测试"""
def test_system_prompt_renders_correctly(self):
template = SystemPromptTemplate(
role="research_assistant",
tools=["search", "calculator"],
constraints=["cite sources", "be concise"]
)
rendered = template.render()
assert "research_assistant" in rendered
assert "search" in rendered
assert "calculator" in rendered
assert "cite sources" in rendered
def test_few_shot_template_with_examples(self):
template = FewShotTemplate(
system="You are a classifier",
examples=[
{"input": "I love it", "output": "positive"},
{"input": "Terrible", "output": "negative"},
],
query="{user_input}"
)
rendered = template.render(user_input="Amazing!")
assert "positive" in rendered
assert "negative" in rendered
assert "Amazing!" in rendered
class TestToolParsing:
"""工具调用解析测试"""
@pytest.mark.parametrize("raw_output,expected_tool,expected_args", [
('{"tool": "search", "args": {"q": "weather"}}', "search", {"q": "weather"}),
('```json\n{"tool": "calc", "args": {"expr": "1+1"}}\n```', "calc", {"expr": "1+1"}),
('I\'ll use the search tool: {"tool": "search", "args": {"q": "news"}}', "search", {"q": "news"}),
])
def test_parse_tool_call(self, raw_output, expected_tool, expected_args):
result = parse_tool_call(raw_output)
assert result.tool == expected_tool
assert result.args == expected_args
def test_parse_malformed_output(self):
with pytest.raises(ToolParseError):
parse_tool_call("This is not JSON at all")
三、第二层:组件测试(Mock LLM)
使用 Mock LLM 测试 Agent 的控制流,确保工作流逻辑正确。
from unittest.mock import AsyncMock, patch
class TestAgentWorkflow:
"""Agent 工作流逻辑测试(不调用真实 LLM)"""
@pytest.fixture
def mock_llm(self):
llm = AsyncMock()
# 预设 LLM 返回序列
llm.side_effect = [
MockResponse(content="I'll search for that", tool_calls=[
ToolCall(tool="search", args={"q": "test query"})
]),
MockResponse(content="Based on the results...", tool_calls=[]),
]
return llm
async def test_agent_calls_search_then_responds(self, mock_llm):
agent = Agent(llm=mock_llm, tools=[search_tool])
result = await agent.run("What is the latest news?")
# 验证调用序列
assert mock_llm.call_count == 2
assert mock_llm.call_args_list[0].args[0].messages[-1].content == "What is the latest news?"
# 验证工具被调用
assert "search" in agent.execution_trace[0].tool_calls[0].tool
async def test_agent_respects_max_iterations(self):
"""测试 Agent 不会无限循环"""
mock_llm = AsyncMock()
mock_llm.return_value = MockResponse(
content="Let me search again",
tool_calls=[ToolCall(tool="search", args={"q": "test"})]
)
agent = Agent(llm=mock_llm, tools=[search_tool], max_iterations=5)
result = await agent.run("Infinite loop test")
assert mock_llm.call_count == 5 # 恰好 5 次
assert result.status == "max_iterations_reached"
async def test_agent_handles_tool_error_gracefully(self):
"""测试工具出错时 Agent 能优雅处理"""
mock_llm = AsyncMock()
mock_llm.side_effect = [
MockResponse(content="", tool_calls=[
ToolCall(tool="failing_tool", args={})
]),
MockResponse(content="I encountered an error, let me try another approach", tool_calls=[]),
]
agent = Agent(llm=mock_llm, tools=[failing_tool])
result = await agent.run("Test error handling")
assert result.status == "success"
assert len(agent.execution_trace) == 2
四、第三层:LLM 评估测试
使用 LLM-as-Judge 对 Agent 输出进行质量评估。这是 Agent 测试的核心层。
from pydantic import BaseModel
class EvaluationResult(BaseModel):
score: float # 0-1
reasoning: str
passed: bool
class LLMJudge:
"""LLM 评判器"""
def __init__(self, judge_model: str = "gpt-5"):
self.model = judge_model
self.client = OpenAI()
async def evaluate(
self,
task: str,
output: str,
criteria: list[str],
reference: str | None = None
) -> EvaluationResult:
prompt = f"""
Task: {task}
Output to evaluate:
{output}
Criteria:
{chr(10).join(f'- {c}' for c in criteria)}
{"Reference answer:" + reference if reference else ""}
Score from 0.0 to 1.0. Be strict.
Respond in JSON: {{"score": float, "reasoning": str, "passed": bool}}
"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
temperature=0.0 # 评判需要确定性
)
return EvaluationResult(**json.loads(response.choices[0].message.content))
# 测试用例定义
TEST_CASES = [
{
"id": "research_001",
"task": "Research the latest developments in quantum computing",
"criteria": [
"Response mentions at least 2 recent breakthroughs (2025-2026)",
"Response includes credible sources",
"Response is under 500 words",
"Response does not hallucinate facts",
],
"min_score": 0.8,
},
{
"id": "code_001",
"task": "Write a Python function to detect cycle in linked list",
"criteria": [
"Code is syntactically correct",
"Algorithm has O(n) time complexity",
"Code handles edge cases (empty list, single node)",
"Code includes type hints",
],
"min_score": 0.85,
},
]
@pytest.mark.parametrize("case", TEST_CASES)
@pytest.mark.asyncio
async def test_agent_quality(case, agent, judge):
"""LLM 评判的 Agent 质量测试"""
output = await agent.run(case["task"])
result = await judge.evaluate(
task=case["task"],
output=output,
criteria=case["criteria"]
)
assert result.score >= case["min_score"], \
f"Score {result.score} < {case['min_score']}. Reason: {result.reasoning}"
五、第四层:集成测试
测试多 Agent 协作场景。
class TestMultiAgentIntegration:
"""多 Agent 集成测试"""
async def test_research_writing_pipeline(self):
"""研究 Agent + 写作 Agent 的协作"""
researcher = Agent(
name="researcher",
llm=real_llm, # 使用真实 LLM
tools=[web_search, paper_search]
)
writer = Agent(
name="writer",
llm=real_llm,
tools=[grammar_check]
)
pipeline = Pipeline([researcher, writer])
result = await pipeline.run("Write a blog about AGI in 2026")
# 结构性验证
assert len(result) > 500
assert len(result) < 3000
assert "AGI" in result
assert "2026" in result
# 质量验证
eval = await judge.evaluate(
task="Write a blog about AGI in 2026",
output=result,
criteria=["well-structured", "factual", "engaging"]
)
assert eval.score > 0.75
六、CI/CD Pipeline 集成
GitHub Actions 配置
# .github/workflows/agent-tests.yml
name: Agent Test Suite
on:
pull_request:
paths:
- "agents/**"
- "prompts/**"
- "tools/**"
schedule:
- cron: "0 6 * * *" # 每天定时全量测试
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- run: pip install -e ".[test]"
- run: pytest tests/unit/ -v --cov=agents --cov-report=xml
component-tests:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- uses: actions/checkout@v4
- run: pytest tests/component/ -v
llm-eval-tests:
runs-on: ubuntu-latest
needs: component-tests
if: github.event_name == 'pull_request'
strategy:
matrix:
test_group: ["research", "coding", "reasoning", "safety"]
steps:
- uses: actions/checkout@v4
- run: pytest tests/evaluation/ -v --group=${{ matrix.test_group }}
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Upload eval results
uses: actions/upload-artifact@v4
with:
name: eval-results-${{ matrix.test_group }}
path: test-results/
regression-check:
runs-on: ubuntu-latest
needs: llm-eval-tests
steps:
- name: Compare with baseline
run: |
python scripts/eval_regression.py \
--current test-results/ \
--baseline eval-baselines/$(git merge-base HEAD main)/ \
--threshold 0.05
回归检测脚本
class EvalRegression:
"""评估回归检测"""
def compare(
self,
current: dict[str, float],
baseline: dict[str, float],
threshold: float = 0.05
) -> bool:
"""如果任何指标下降超过 threshold,返回 True(存在回归)"""
regressions = []
for test_id, score in current.items():
baseline_score = baseline.get(test_id)
if baseline_score is None:
continue
if score < baseline_score - threshold:
regressions.append({
"test": test_id,
"baseline": baseline_score,
"current": score,
"drop": baseline_score - score
})
if regressions:
print("⚠️ Evaluation regression detected:")
for r in regressions:
print(f" {r['test']}: {r['baseline']:.2f} → {r['current']:.2f} (↓{r['drop']:.2f})")
return True
return False
七、测试数据管理
class TestDataset:
"""测试数据集管理"""
def __init__(self, version: str = "v1.0"):
self.version = version
self.cases = self._load()
def _load(self) -> list[TestCase]:
"""从版本化的测试数据文件加载"""
path = f"testdata/{self.version}/cases.jsonl"
cases = []
with open(path) as f:
for line in f:
data = json.loads(line)
cases.append(TestCase(**data))
return cases
def split(self, ratio: float = 0.8):
"""分割为开发和回归集"""
shuffled = sorted(self.cases, key=lambda x: hash(x.id))
split_at = int(len(shuffled) * ratio)
return shuffled[:split_at], shuffled[split_at:]
八、测试成本控制
LLM 测试的最大挑战是成本。推荐策略:
| 策略 | 节省比例 | 实现方式 |
|---|---|---|
| 分层执行 | 70% | PR 只跑单元+组件层,合并后跑评估层 |
| 模型降级 | 85% | 评估测试用 GPT-4o-mini 代替 GPT-5 |
| 采样测试 | 60% | 从 100 个用例中随机抽 30 个 |
| 缓存 Mock | 90% | 缓存 LLM 响应,重复测试用缓存 |
| 并行化 | 0%(提速) | 评估用例并行执行 |
结语
Agent 评估自动化不是一次性工程,而是持续演进的体系。从确定性单元测试到 LLM-as-Judge 评估,每一层都在补充其他层的盲区。关键原则是:能确定测试的就不要用 LLM 评判,能用便宜模型评判的就不要用昂贵模型。让每一分测试预算都花在真正需要的地方。
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。
