mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
docs: 添加 AI Agent 开发指南和测试策略文档
This commit is contained in:
@@ -0,0 +1,515 @@
|
||||
# ProxyCast AI Agent 开发指南
|
||||
|
||||
> 面向未来 AI Agent 功能开发的架构设计与最佳实践
|
||||
|
||||
## 一、愿景与定位
|
||||
|
||||
ProxyCast 的未来方向是从 **API 代理工具** 演进为 **AI Agent 创作平台**。
|
||||
核心目标是让用户能够:
|
||||
|
||||
1. **创建自定义 Agent** - 定义 Agent 的能力、工具、行为
|
||||
2. **编排 Agent 工作流** - 多 Agent 协作、任务分解
|
||||
3. **本地执行 Agent** - 利用本地资源执行工具调用
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ProxyCast AI Agent 平台 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Agent 定义 │ │ 工作流编排 │ │ 工具执行 │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • 系统提示词 │ │ • 任务分解 │ │ • 文件操作 │ │
|
||||
│ │ • 工具配置 │ │ • 条件分支 │ │ • Shell 命令 │ │
|
||||
│ │ • 行为约束 │ │ • 循环控制 │ │ • API 调用 │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ ↓ ↓ ↓ │
|
||||
│ ═══════════════════════════════════════════════════════════ │
|
||||
│ 统一的 Agent 运行时 │
|
||||
│ ═══════════════════════════════════════════════════════════ │
|
||||
│ ↓ ↓ ↓ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Kiro │ │ Gemini │ │ Qwen │ │
|
||||
│ │ Provider │ │ Provider │ │ Provider │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、核心架构设计
|
||||
|
||||
### 2.1 Agent 定义层
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/definition.rs
|
||||
|
||||
/// Agent 定义
|
||||
pub struct AgentDefinition {
|
||||
/// 唯一标识
|
||||
pub id: String,
|
||||
/// 显示名称
|
||||
pub name: String,
|
||||
/// 系统提示词
|
||||
pub system_prompt: String,
|
||||
/// 可用工具列表
|
||||
pub tools: Vec<ToolDefinition>,
|
||||
/// 行为约束
|
||||
pub constraints: AgentConstraints,
|
||||
/// 使用的 Provider
|
||||
pub provider: ProviderType,
|
||||
}
|
||||
|
||||
/// 工具定义
|
||||
pub struct ToolDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: serde_json::Value, // JSON Schema
|
||||
pub handler: ToolHandler,
|
||||
}
|
||||
|
||||
/// 行为约束
|
||||
pub struct AgentConstraints {
|
||||
/// 最大工具调用次数
|
||||
pub max_tool_calls: u32,
|
||||
/// 最大对话轮数
|
||||
pub max_turns: u32,
|
||||
/// 超时时间(秒)
|
||||
pub timeout_seconds: u32,
|
||||
/// 是否允许并行工具调用
|
||||
pub allow_parallel_tools: bool,
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 工具执行层
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tools/mod.rs
|
||||
|
||||
/// 工具执行器 trait
|
||||
#[async_trait]
|
||||
pub trait ToolExecutor: Send + Sync {
|
||||
/// 执行工具
|
||||
async fn execute(&self, input: ToolInput) -> Result<ToolOutput, ToolError>;
|
||||
|
||||
/// 工具名称
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// 工具描述
|
||||
fn description(&self) -> &str;
|
||||
|
||||
/// 参数 Schema
|
||||
fn parameters_schema(&self) -> serde_json::Value;
|
||||
}
|
||||
|
||||
/// 内置工具
|
||||
pub mod builtin {
|
||||
pub struct ReadFileTool;
|
||||
pub struct WriteFileTool;
|
||||
pub struct ShellCommandTool;
|
||||
pub struct HttpRequestTool;
|
||||
pub struct SearchFilesTool;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Agent 运行时
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/runtime.rs
|
||||
|
||||
/// Agent 运行时
|
||||
pub struct AgentRuntime {
|
||||
/// Agent 定义
|
||||
definition: AgentDefinition,
|
||||
/// Provider 客户端
|
||||
provider: Box<dyn ProviderClient>,
|
||||
/// 工具执行器
|
||||
tools: HashMap<String, Box<dyn ToolExecutor>>,
|
||||
/// 对话历史
|
||||
messages: Vec<Message>,
|
||||
/// 运行状态
|
||||
state: AgentState,
|
||||
}
|
||||
|
||||
impl AgentRuntime {
|
||||
/// 执行 Agent 循环
|
||||
pub async fn run(&mut self, user_input: &str) -> Result<AgentResponse, AgentError> {
|
||||
self.messages.push(Message::user(user_input));
|
||||
|
||||
loop {
|
||||
// 1. 调用 LLM
|
||||
let response = self.provider.chat(&self.messages).await?;
|
||||
|
||||
// 2. 检查是否有工具调用
|
||||
if let Some(tool_calls) = response.tool_calls {
|
||||
// 3. 执行工具
|
||||
let results = self.execute_tools(tool_calls).await?;
|
||||
|
||||
// 4. 将结果加入对话
|
||||
self.messages.extend(results);
|
||||
|
||||
// 5. 检查约束
|
||||
if self.check_constraints().is_err() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// 没有工具调用,返回最终响应
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 三、消息格式与协议转换
|
||||
|
||||
### 3.1 统一消息格式
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/messages.rs
|
||||
|
||||
/// 统一消息格式(内部使用)
|
||||
pub enum Message {
|
||||
System { content: String },
|
||||
User { content: Vec<ContentBlock> },
|
||||
Assistant { content: Vec<ContentBlock>, tool_calls: Option<Vec<ToolCall>> },
|
||||
ToolResult { tool_use_id: String, content: String, is_error: bool },
|
||||
}
|
||||
|
||||
/// 内容块
|
||||
pub enum ContentBlock {
|
||||
Text { text: String },
|
||||
Image { source: ImageSource },
|
||||
ToolUse { id: String, name: String, input: serde_json::Value },
|
||||
ToolResult { tool_use_id: String, content: String },
|
||||
}
|
||||
|
||||
/// 工具调用
|
||||
pub struct ToolCall {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub input: serde_json::Value,
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 协议转换器
|
||||
|
||||
```rust
|
||||
// src-tauri/src/converter/mod.rs
|
||||
|
||||
/// 协议转换器 trait
|
||||
pub trait ProtocolConverter {
|
||||
/// 转换为 Provider 格式
|
||||
fn to_provider(&self, messages: &[Message]) -> ProviderRequest;
|
||||
|
||||
/// 从 Provider 格式转换
|
||||
fn from_provider(&self, response: ProviderResponse) -> Message;
|
||||
|
||||
/// 转换工具定义
|
||||
fn convert_tools(&self, tools: &[ToolDefinition]) -> Vec<ProviderTool>;
|
||||
}
|
||||
|
||||
/// OpenAI 格式转换器
|
||||
pub struct OpenAIConverter;
|
||||
|
||||
/// Claude 格式转换器
|
||||
pub struct ClaudeConverter;
|
||||
|
||||
/// Gemini 格式转换器
|
||||
pub struct GeminiConverter;
|
||||
```
|
||||
|
||||
### 3.3 流式响应处理
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/streaming.rs
|
||||
|
||||
/// 流式响应处理器
|
||||
pub struct StreamProcessor {
|
||||
/// 当前状态
|
||||
state: StreamState,
|
||||
/// 缓冲区
|
||||
buffer: String,
|
||||
/// 工具调用缓冲
|
||||
tool_buffer: Option<PartialToolCall>,
|
||||
}
|
||||
|
||||
impl StreamProcessor {
|
||||
/// 处理流式数据块
|
||||
pub fn process_chunk(&mut self, chunk: &str) -> Vec<StreamEvent> {
|
||||
let mut events = Vec::new();
|
||||
|
||||
// 解析数据块
|
||||
// 处理文本、工具调用等
|
||||
// 生成事件
|
||||
|
||||
events
|
||||
}
|
||||
}
|
||||
|
||||
/// 流式事件
|
||||
pub enum StreamEvent {
|
||||
TextDelta { text: String },
|
||||
ToolCallStart { id: String, name: String },
|
||||
ToolCallDelta { id: String, input_delta: String },
|
||||
ToolCallEnd { id: String },
|
||||
Done,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、工具系统设计
|
||||
|
||||
### 4.1 内置工具
|
||||
|
||||
| 工具 | 描述 | 参数 |
|
||||
|------|------|------|
|
||||
| `read_file` | 读取文件内容 | `path: string` |
|
||||
| `write_file` | 写入文件 | `path: string, content: string` |
|
||||
| `list_directory` | 列出目录内容 | `path: string, pattern?: string` |
|
||||
| `search_files` | 搜索文件内容 | `pattern: string, path?: string` |
|
||||
| `shell_command` | 执行 Shell 命令 | `command: string, cwd?: string` |
|
||||
| `http_request` | 发送 HTTP 请求 | `url: string, method: string, ...` |
|
||||
|
||||
### 4.2 工具注册机制
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tools/registry.rs
|
||||
|
||||
/// 工具注册表
|
||||
pub struct ToolRegistry {
|
||||
tools: HashMap<String, Box<dyn ToolExecutor>>,
|
||||
}
|
||||
|
||||
impl ToolRegistry {
|
||||
/// 注册内置工具
|
||||
pub fn register_builtin(&mut self) {
|
||||
self.register(Box::new(ReadFileTool::new()));
|
||||
self.register(Box::new(WriteFileTool::new()));
|
||||
self.register(Box::new(ShellCommandTool::new()));
|
||||
// ...
|
||||
}
|
||||
|
||||
/// 注册自定义工具
|
||||
pub fn register(&mut self, tool: Box<dyn ToolExecutor>) {
|
||||
self.tools.insert(tool.name().to_string(), tool);
|
||||
}
|
||||
|
||||
/// 获取工具
|
||||
pub fn get(&self, name: &str) -> Option<&dyn ToolExecutor> {
|
||||
self.tools.get(name).map(|t| t.as_ref())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 工具执行安全
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tools/security.rs
|
||||
|
||||
/// 工具执行安全策略
|
||||
pub struct SecurityPolicy {
|
||||
/// 允许的路径前缀
|
||||
pub allowed_paths: Vec<PathBuf>,
|
||||
/// 禁止的命令
|
||||
pub blocked_commands: Vec<String>,
|
||||
/// 允许的 HTTP 域名
|
||||
pub allowed_domains: Vec<String>,
|
||||
/// 最大文件大小
|
||||
pub max_file_size: u64,
|
||||
}
|
||||
|
||||
impl SecurityPolicy {
|
||||
/// 检查文件路径是否允许
|
||||
pub fn check_path(&self, path: &Path) -> Result<(), SecurityError> {
|
||||
// 检查路径是否在允许范围内
|
||||
// 防止路径遍历攻击
|
||||
}
|
||||
|
||||
/// 检查命令是否允许
|
||||
pub fn check_command(&self, command: &str) -> Result<(), SecurityError> {
|
||||
// 检查命令是否在黑名单中
|
||||
// 检查危险操作
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、状态管理
|
||||
|
||||
### 5.1 Agent 状态
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/state.rs
|
||||
|
||||
/// Agent 运行状态
|
||||
pub struct AgentState {
|
||||
/// 当前阶段
|
||||
pub phase: AgentPhase,
|
||||
/// 工具调用计数
|
||||
pub tool_call_count: u32,
|
||||
/// 对话轮数
|
||||
pub turn_count: u32,
|
||||
/// 开始时间
|
||||
pub started_at: Instant,
|
||||
/// Token 使用量
|
||||
pub token_usage: TokenUsage,
|
||||
}
|
||||
|
||||
/// Agent 阶段
|
||||
pub enum AgentPhase {
|
||||
Idle,
|
||||
Thinking,
|
||||
ToolExecution { tool_name: String },
|
||||
WaitingForUser,
|
||||
Completed,
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
/// Token 使用量
|
||||
pub struct TokenUsage {
|
||||
pub input_tokens: u32,
|
||||
pub output_tokens: u32,
|
||||
pub cache_read_tokens: u32,
|
||||
pub cache_write_tokens: u32,
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 前端状态同步
|
||||
|
||||
```typescript
|
||||
// src/stores/agentStore.ts
|
||||
|
||||
interface AgentState {
|
||||
// Agent 定义
|
||||
agents: AgentDefinition[];
|
||||
currentAgent: string | null;
|
||||
|
||||
// 运行状态
|
||||
isRunning: boolean;
|
||||
phase: AgentPhase;
|
||||
messages: Message[];
|
||||
|
||||
// 统计
|
||||
tokenUsage: TokenUsage;
|
||||
toolCallCount: number;
|
||||
}
|
||||
|
||||
// 使用 Zustand 管理状态
|
||||
export const useAgentStore = create<AgentState>((set, get) => ({
|
||||
// 初始状态
|
||||
agents: [],
|
||||
currentAgent: null,
|
||||
isRunning: false,
|
||||
phase: 'idle',
|
||||
messages: [],
|
||||
tokenUsage: { input: 0, output: 0 },
|
||||
toolCallCount: 0,
|
||||
|
||||
// Actions
|
||||
startAgent: async (agentId: string, input: string) => {
|
||||
set({ isRunning: true, phase: 'thinking' });
|
||||
// 调用 Tauri 命令
|
||||
await invoke('run_agent', { agentId, input });
|
||||
},
|
||||
|
||||
stopAgent: async () => {
|
||||
await invoke('stop_agent');
|
||||
set({ isRunning: false, phase: 'idle' });
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 六、开发路线图
|
||||
|
||||
### Phase 1: 基础 Agent 框架(2-3 周)
|
||||
|
||||
- [ ] Agent 定义数据结构
|
||||
- [ ] 基础工具执行器(read_file, write_file, shell_command)
|
||||
- [ ] Agent 运行时循环
|
||||
- [ ] 简单的 CLI 测试界面
|
||||
|
||||
### Phase 2: 工具系统完善(2-3 周)
|
||||
|
||||
- [ ] 工具注册机制
|
||||
- [ ] 安全策略实现
|
||||
- [ ] 更多内置工具(search_files, http_request)
|
||||
- [ ] 工具执行日志
|
||||
|
||||
### Phase 3: 流式响应(1-2 周)
|
||||
|
||||
- [ ] 流式响应处理器
|
||||
- [ ] 前端实时显示
|
||||
- [ ] 工具调用进度展示
|
||||
|
||||
### Phase 4: Agent 管理 UI(2-3 周)
|
||||
|
||||
- [ ] Agent 创建/编辑界面
|
||||
- [ ] 工具配置界面
|
||||
- [ ] 对话历史查看
|
||||
- [ ] 运行状态监控
|
||||
|
||||
### Phase 5: 高级功能(持续)
|
||||
|
||||
- [ ] 多 Agent 协作
|
||||
- [ ] 工作流编排
|
||||
- [ ] 自定义工具插件
|
||||
- [ ] Agent 模板市场
|
||||
|
||||
---
|
||||
|
||||
## 七、设计原则
|
||||
|
||||
### 7.1 模块化
|
||||
|
||||
每个组件应该是独立的、可测试的:
|
||||
|
||||
```
|
||||
agent/
|
||||
├── definition.rs # Agent 定义(纯数据结构)
|
||||
├── runtime.rs # 运行时(业务逻辑)
|
||||
├── tools/ # 工具系统(可独立测试)
|
||||
├── messages.rs # 消息格式(纯数据结构)
|
||||
└── state.rs # 状态管理(纯数据结构)
|
||||
```
|
||||
|
||||
### 7.2 可扩展性
|
||||
|
||||
- 工具系统使用 trait,支持自定义工具
|
||||
- Provider 系统已经是可扩展的
|
||||
- 消息格式统一,便于添加新协议
|
||||
|
||||
### 7.3 安全优先
|
||||
|
||||
- 所有工具执行都经过安全检查
|
||||
- 路径访问限制在工作目录内
|
||||
- 命令执行有黑名单机制
|
||||
- 网络请求有域名白名单
|
||||
|
||||
### 7.4 用户体验
|
||||
|
||||
- 流式响应,实时反馈
|
||||
- 清晰的状态展示
|
||||
- 详细的错误信息
|
||||
- 可中断的长时间操作
|
||||
|
||||
---
|
||||
|
||||
## 八、参考资源
|
||||
|
||||
- [Anthropic: Building effective agents](https://www.anthropic.com/research/building-effective-agents)
|
||||
- [OpenAI: Function calling](https://platform.openai.com/docs/guides/function-calling)
|
||||
- [LangChain: Agent concepts](https://python.langchain.com/docs/concepts/agents/)
|
||||
- [Claude Code: Agent architecture](https://github.com/anthropics/claude-code)
|
||||
|
||||
---
|
||||
|
||||
*本文档定义了 ProxyCast AI Agent 功能的架构设计,随着开发进展会持续更新。*
|
||||
@@ -0,0 +1,792 @@
|
||||
# ProxyCast AI Agent 测试指南
|
||||
|
||||
> 基于 Anthropic 评估理论与实践经验的 AI Agent 测试策略
|
||||
|
||||
## 一、为什么 AI Agent 需要专门的测试策略?
|
||||
|
||||
AI Agent 与传统软件的关键区别:
|
||||
|
||||
| 特性 | 传统软件 | AI Agent |
|
||||
|------|----------|----------|
|
||||
| 输出确定性 | 给定输入,输出固定 | 输出可能变化 |
|
||||
| 执行路径 | 可预测 | 自主决策,路径不确定 |
|
||||
| 错误传播 | 局部影响 | 可能累积和放大 |
|
||||
| 成功标准 | 明确的对错 | 可能有多个有效解 |
|
||||
|
||||
**核心挑战**:如何测试一个"可能找到比你预期更好的解决方案"的系统?
|
||||
|
||||
---
|
||||
|
||||
## 二、测试金字塔
|
||||
|
||||
```
|
||||
┌─────────┐
|
||||
│ E2E │ ← 少量:完整 Agent 流程
|
||||
│ Tests │
|
||||
─┴─────────┴─
|
||||
┌─────────────┐
|
||||
│ Integration │ ← 中等:组件交互
|
||||
│ Tests │
|
||||
─┴─────────────┴─
|
||||
┌─────────────────┐
|
||||
│ Unit Tests │ ← 大量:单个函数
|
||||
│ │
|
||||
─┴─────────────────┴─
|
||||
```
|
||||
|
||||
### 2.1 单元测试(70%)
|
||||
|
||||
测试独立的、确定性的组件:
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/message_tests.rs
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_message_serialization() {
|
||||
let msg = Message::User {
|
||||
content: vec![ContentBlock::Text {
|
||||
text: "Hello".to_string()
|
||||
}],
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&msg).unwrap();
|
||||
let parsed: Message = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(msg, parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_call_parsing() {
|
||||
let input = r#"{"name": "read_file", "input": {"path": "/test.txt"}}"#;
|
||||
let tool_call: ToolCall = serde_json::from_str(input).unwrap();
|
||||
|
||||
assert_eq!(tool_call.name, "read_file");
|
||||
assert_eq!(tool_call.input["path"], "/test.txt");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 集成测试(20%)
|
||||
|
||||
测试组件之间的交互:
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/tool_integration_tests.rs
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_write_file_integration() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_path = temp_dir.path().join("test.txt");
|
||||
|
||||
// 写入文件
|
||||
let write_tool = WriteFileTool::new();
|
||||
let write_result = write_tool.execute(ToolInput {
|
||||
name: "write_file".to_string(),
|
||||
input: json!({
|
||||
"path": file_path.to_str().unwrap(),
|
||||
"content": "Hello, World!"
|
||||
}),
|
||||
}).await.unwrap();
|
||||
|
||||
assert!(write_result.success);
|
||||
|
||||
// 读取文件
|
||||
let read_tool = ReadFileTool::new();
|
||||
let read_result = read_tool.execute(ToolInput {
|
||||
name: "read_file".to_string(),
|
||||
input: json!({
|
||||
"path": file_path.to_str().unwrap()
|
||||
}),
|
||||
}).await.unwrap();
|
||||
|
||||
assert_eq!(read_result.content, "Hello, World!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 端到端测试(10%)
|
||||
|
||||
测试完整的 Agent 流程:
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/e2e_tests.rs
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_agent_simple_task() {
|
||||
// 创建 Mock Provider
|
||||
let mock_provider = MockProvider::new()
|
||||
.with_response("I'll read the file for you.")
|
||||
.with_tool_call("read_file", json!({"path": "/test.txt"}))
|
||||
.with_response("The file contains: Hello!");
|
||||
|
||||
// 创建 Agent
|
||||
let agent = AgentRuntime::new(
|
||||
test_agent_definition(),
|
||||
Box::new(mock_provider),
|
||||
);
|
||||
|
||||
// 运行 Agent
|
||||
let result = agent.run("Read the file /test.txt").await.unwrap();
|
||||
|
||||
// 验证结果
|
||||
assert!(result.content.contains("Hello"));
|
||||
assert_eq!(agent.state.tool_call_count, 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 三、三种评分器类型
|
||||
|
||||
### 3.1 代码评分器(确定性验证)
|
||||
|
||||
适用于有明确对错的场景:
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/graders/code_grader.rs
|
||||
|
||||
/// 代码评分器
|
||||
pub struct CodeGrader;
|
||||
|
||||
impl CodeGrader {
|
||||
/// 验证文件是否存在
|
||||
pub fn file_exists(path: &Path) -> bool {
|
||||
path.exists()
|
||||
}
|
||||
|
||||
/// 验证文件内容包含指定字符串
|
||||
pub fn file_contains(path: &Path, expected: &str) -> bool {
|
||||
if let Ok(content) = std::fs::read_to_string(path) {
|
||||
content.contains(expected)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证 JSON 结构
|
||||
pub fn json_matches_schema(json: &str, schema: &str) -> bool {
|
||||
// 使用 JSON Schema 验证
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// 验证工具调用次数
|
||||
pub fn tool_call_count(state: &AgentState, expected: u32) -> bool {
|
||||
state.tool_call_count == expected
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 模型评分器(灵活判断)
|
||||
|
||||
适用于开放式任务:
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/graders/model_grader.rs
|
||||
|
||||
/// 模型评分器
|
||||
pub struct ModelGrader {
|
||||
provider: Box<dyn ProviderClient>,
|
||||
}
|
||||
|
||||
impl ModelGrader {
|
||||
/// 使用 LLM 评估响应质量
|
||||
pub async fn evaluate_response(
|
||||
&self,
|
||||
task: &str,
|
||||
response: &str,
|
||||
rubric: &str,
|
||||
) -> GradeResult {
|
||||
let prompt = format!(
|
||||
r#"你是一个评估 AI Agent 响应质量的评分器。
|
||||
|
||||
任务描述:
|
||||
{task}
|
||||
|
||||
Agent 响应:
|
||||
{response}
|
||||
|
||||
评分标准:
|
||||
{rubric}
|
||||
|
||||
请根据评分标准,给出 1-5 分的评分,并说明理由。
|
||||
输出格式:
|
||||
{{"score": <1-5>, "reason": "<理由>"}}
|
||||
"#
|
||||
);
|
||||
|
||||
let result = self.provider.chat(&[Message::user(&prompt)]).await?;
|
||||
// 解析评分结果
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// 评分结果
|
||||
pub struct GradeResult {
|
||||
pub score: u8, // 1-5
|
||||
pub reason: String,
|
||||
pub passed: bool, // score >= 3
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 人工评分器(金标准)
|
||||
|
||||
用于校准模型评分器:
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/graders/human_grader.rs
|
||||
|
||||
/// 人工评分记录
|
||||
pub struct HumanGrade {
|
||||
pub task_id: String,
|
||||
pub response: String,
|
||||
pub score: u8,
|
||||
pub feedback: String,
|
||||
pub grader_id: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 人工评分收集器
|
||||
pub struct HumanGradeCollector {
|
||||
grades: Vec<HumanGrade>,
|
||||
}
|
||||
|
||||
impl HumanGradeCollector {
|
||||
/// 计算评分者一致性
|
||||
pub fn inter_rater_agreement(&self) -> f64 {
|
||||
// 计算 Cohen's Kappa 或 Fleiss' Kappa
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// 导出用于校准模型评分器
|
||||
pub fn export_for_calibration(&self) -> Vec<CalibrationSample> {
|
||||
self.grades.iter().map(|g| CalibrationSample {
|
||||
task: g.task_id.clone(),
|
||||
response: g.response.clone(),
|
||||
human_score: g.score,
|
||||
}).collect()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、测试场景设计
|
||||
|
||||
### 4.1 能力测试(Capability Evals)
|
||||
|
||||
测试 Agent 能做什么:
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/capability_tests.rs
|
||||
|
||||
/// 文件操作能力测试
|
||||
mod file_operations {
|
||||
#[tokio::test]
|
||||
async fn test_create_file() {
|
||||
// 任务:创建一个包含特定内容的文件
|
||||
// 预期通过率:低(给团队爬坡空间)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_modify_file() {
|
||||
// 任务:修改现有文件的特定部分
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_and_replace() {
|
||||
// 任务:在多个文件中搜索并替换
|
||||
}
|
||||
}
|
||||
|
||||
/// 代码理解能力测试
|
||||
mod code_understanding {
|
||||
#[tokio::test]
|
||||
async fn test_explain_function() {
|
||||
// 任务:解释一个函数的作用
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_find_bug() {
|
||||
// 任务:找出代码中的 bug
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 回归测试(Regression Evals)
|
||||
|
||||
确保 Agent 不退化:
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/regression_tests.rs
|
||||
|
||||
/// 回归测试套件
|
||||
/// 预期通过率:接近 100%
|
||||
mod regression {
|
||||
#[tokio::test]
|
||||
async fn test_basic_file_read() {
|
||||
// 基础功能:读取文件
|
||||
// 这个测试必须始终通过
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_basic_file_write() {
|
||||
// 基础功能:写入文件
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_basic_shell_command() {
|
||||
// 基础功能:执行简单命令
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 边界测试(Edge Cases)
|
||||
|
||||
测试异常情况:
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/edge_case_tests.rs
|
||||
|
||||
mod edge_cases {
|
||||
#[tokio::test]
|
||||
async fn test_file_not_found() {
|
||||
// 读取不存在的文件
|
||||
// Agent 应该优雅处理,而不是崩溃
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_permission_denied() {
|
||||
// 写入没有权限的目录
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_timeout() {
|
||||
// 长时间运行的命令
|
||||
// Agent 应该在超时后停止
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_max_tool_calls() {
|
||||
// 达到最大工具调用次数
|
||||
// Agent 应该停止并返回部分结果
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 五、处理非确定性
|
||||
|
||||
### 5.1 pass@k 与 pass^k
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/metrics.rs
|
||||
|
||||
/// 计算 pass@k:至少一次成功的概率
|
||||
pub fn pass_at_k(success_rate: f64, k: u32) -> f64 {
|
||||
1.0 - (1.0 - success_rate).powi(k as i32)
|
||||
}
|
||||
|
||||
/// 计算 pass^k:全部成功的概率
|
||||
pub fn pass_power_k(success_rate: f64, k: u32) -> f64 {
|
||||
success_rate.powi(k as i32)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_metrics() {
|
||||
let p = 0.75; // 单次成功率 75%
|
||||
|
||||
// k=1 时两者相等
|
||||
assert!((pass_at_k(p, 1) - pass_power_k(p, 1)).abs() < 0.001);
|
||||
|
||||
// k=3 时
|
||||
assert!((pass_at_k(p, 3) - 0.984).abs() < 0.01); // 98.4%
|
||||
assert!((pass_power_k(p, 3) - 0.422).abs() < 0.01); // 42.2%
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 多次试验
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/trial_runner.rs
|
||||
|
||||
/// 试验运行器
|
||||
pub struct TrialRunner {
|
||||
agent: AgentRuntime,
|
||||
trials: u32,
|
||||
}
|
||||
|
||||
impl TrialRunner {
|
||||
/// 运行多次试验
|
||||
pub async fn run_trials(&self, task: &str) -> TrialResults {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for i in 0..self.trials {
|
||||
let result = self.agent.run(task).await;
|
||||
results.push(TrialResult {
|
||||
trial_id: i,
|
||||
success: result.is_ok(),
|
||||
response: result.ok(),
|
||||
error: result.err(),
|
||||
});
|
||||
}
|
||||
|
||||
TrialResults::from(results)
|
||||
}
|
||||
}
|
||||
|
||||
/// 试验结果统计
|
||||
pub struct TrialResults {
|
||||
pub total: u32,
|
||||
pub successes: u32,
|
||||
pub failures: u32,
|
||||
pub success_rate: f64,
|
||||
pub pass_at_1: f64,
|
||||
pub pass_at_3: f64,
|
||||
pub pass_power_3: f64,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、Mock 与测试替身
|
||||
|
||||
### 6.1 Mock Provider
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/mocks/mock_provider.rs
|
||||
|
||||
/// Mock Provider 用于测试
|
||||
pub struct MockProvider {
|
||||
responses: VecDeque<MockResponse>,
|
||||
}
|
||||
|
||||
enum MockResponse {
|
||||
Text(String),
|
||||
ToolCall { name: String, input: serde_json::Value },
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl MockProvider {
|
||||
pub fn new() -> Self {
|
||||
Self { responses: VecDeque::new() }
|
||||
}
|
||||
|
||||
pub fn with_response(mut self, text: &str) -> Self {
|
||||
self.responses.push_back(MockResponse::Text(text.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_tool_call(mut self, name: &str, input: serde_json::Value) -> Self {
|
||||
self.responses.push_back(MockResponse::ToolCall {
|
||||
name: name.to_string(),
|
||||
input,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_error(mut self, error: &str) -> Self {
|
||||
self.responses.push_back(MockResponse::Error(error.to_string()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ProviderClient for MockProvider {
|
||||
async fn chat(&self, messages: &[Message]) -> Result<Response, ProviderError> {
|
||||
match self.responses.pop_front() {
|
||||
Some(MockResponse::Text(text)) => Ok(Response::text(text)),
|
||||
Some(MockResponse::ToolCall { name, input }) => {
|
||||
Ok(Response::tool_call(name, input))
|
||||
}
|
||||
Some(MockResponse::Error(e)) => Err(ProviderError::Api(e)),
|
||||
None => Err(ProviderError::Api("No more mock responses".to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Mock 文件系统
|
||||
|
||||
```rust
|
||||
// src-tauri/src/agent/tests/mocks/mock_fs.rs
|
||||
|
||||
/// Mock 文件系统
|
||||
pub struct MockFileSystem {
|
||||
files: HashMap<PathBuf, String>,
|
||||
}
|
||||
|
||||
impl MockFileSystem {
|
||||
pub fn new() -> Self {
|
||||
Self { files: HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn with_file(mut self, path: &str, content: &str) -> Self {
|
||||
self.files.insert(PathBuf::from(path), content.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn read(&self, path: &Path) -> Result<String, std::io::Error> {
|
||||
self.files.get(path)
|
||||
.cloned()
|
||||
.ok_or_else(|| std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"File not found"
|
||||
))
|
||||
}
|
||||
|
||||
pub fn write(&mut self, path: &Path, content: &str) -> Result<(), std::io::Error> {
|
||||
self.files.insert(path.to_path_buf(), content.to_string());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、测试组织
|
||||
|
||||
### 7.1 目录结构
|
||||
|
||||
```
|
||||
src-tauri/
|
||||
├── src/
|
||||
│ └── agent/
|
||||
│ ├── mod.rs
|
||||
│ ├── runtime.rs
|
||||
│ ├── tools/
|
||||
│ └── tests/ # 单元测试
|
||||
│ ├── mod.rs
|
||||
│ ├── message_tests.rs
|
||||
│ ├── tool_tests.rs
|
||||
│ └── mocks/
|
||||
│ ├── mod.rs
|
||||
│ ├── mock_provider.rs
|
||||
│ └── mock_fs.rs
|
||||
└── tests/ # 集成测试
|
||||
├── agent_integration.rs
|
||||
├── e2e_tests.rs
|
||||
└── fixtures/
|
||||
├── test_files/
|
||||
└── test_agents/
|
||||
```
|
||||
|
||||
### 7.2 测试命令
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
cargo test
|
||||
|
||||
# 运行特定模块测试
|
||||
cargo test agent::tests
|
||||
|
||||
# 运行集成测试
|
||||
cargo test --test agent_integration
|
||||
|
||||
# 运行并显示输出
|
||||
cargo test -- --nocapture
|
||||
|
||||
# 运行特定测试
|
||||
cargo test test_read_file
|
||||
```
|
||||
|
||||
### 7.3 CI 配置
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-action@stable
|
||||
|
||||
- name: Run unit tests
|
||||
run: cd src-tauri && cargo test
|
||||
|
||||
- name: Run integration tests
|
||||
run: cd src-tauri && cargo test --test '*'
|
||||
|
||||
- name: Run clippy
|
||||
run: cd src-tauri && cargo clippy -- -D warnings
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 八、测试最佳实践
|
||||
|
||||
### 8.1 评估结果,而非路径
|
||||
|
||||
```rust
|
||||
// ❌ 错误:检查具体的工具调用顺序
|
||||
#[tokio::test]
|
||||
async fn test_bad_check_path() {
|
||||
let result = agent.run("Create a file").await.unwrap();
|
||||
|
||||
// 过于严格:Agent 可能找到更好的方法
|
||||
assert_eq!(result.tool_calls[0].name, "check_directory");
|
||||
assert_eq!(result.tool_calls[1].name, "write_file");
|
||||
}
|
||||
|
||||
// ✅ 正确:检查最终结果
|
||||
#[tokio::test]
|
||||
async fn test_good_check_outcome() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_path = temp_dir.path().join("output.txt");
|
||||
|
||||
agent.run(&format!("Create a file at {}", file_path.display())).await.unwrap();
|
||||
|
||||
// 只检查文件是否被创建
|
||||
assert!(file_path.exists());
|
||||
|
||||
// 检查内容是否合理
|
||||
let content = std::fs::read_to_string(&file_path).unwrap();
|
||||
assert!(!content.is_empty());
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 平衡的问题集
|
||||
|
||||
```rust
|
||||
// 测试"应该做"和"不应该做"
|
||||
|
||||
mod security_tests {
|
||||
// 应该允许的操作
|
||||
#[tokio::test]
|
||||
async fn test_should_allow_read_in_workspace() {
|
||||
let result = agent.run("Read /workspace/file.txt").await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// 不应该允许的操作
|
||||
#[tokio::test]
|
||||
async fn test_should_block_read_outside_workspace() {
|
||||
let result = agent.run("Read /etc/passwd").await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// 应该允许的命令
|
||||
#[tokio::test]
|
||||
async fn test_should_allow_safe_commands() {
|
||||
let result = agent.run("Run: ls -la").await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// 不应该允许的命令
|
||||
#[tokio::test]
|
||||
async fn test_should_block_dangerous_commands() {
|
||||
let result = agent.run("Run: rm -rf /").await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 测试隔离
|
||||
|
||||
```rust
|
||||
// 每个测试使用独立的环境
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_isolated_environment() {
|
||||
// 创建临时目录
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
// 创建独立的 Agent 实例
|
||||
let agent = AgentRuntime::new(
|
||||
test_agent_definition(),
|
||||
Box::new(MockProvider::new()),
|
||||
).with_working_dir(temp_dir.path());
|
||||
|
||||
// 运行测试
|
||||
agent.run("Create some files").await.unwrap();
|
||||
|
||||
// temp_dir 在测试结束时自动清理
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 阅读 Transcript
|
||||
|
||||
```rust
|
||||
// 记录完整的执行过程用于调试
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_transcript() {
|
||||
let mut agent = AgentRuntime::new(...);
|
||||
agent.enable_transcript();
|
||||
|
||||
let result = agent.run("Complex task").await;
|
||||
|
||||
// 如果失败,打印完整的执行记录
|
||||
if result.is_err() {
|
||||
println!("=== Transcript ===");
|
||||
for entry in agent.transcript() {
|
||||
println!("{:?}", entry);
|
||||
}
|
||||
println!("==================");
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、总结
|
||||
|
||||
### 核心要点
|
||||
|
||||
1. **分层测试**:单元测试(70%)+ 集成测试(20%)+ E2E 测试(10%)
|
||||
2. **三种评分器**:代码评分器(确定性)、模型评分器(灵活)、人工评分器(校准)
|
||||
3. **处理非确定性**:使用 pass@k 和 pass^k 指标,运行多次试验
|
||||
4. **评估结果而非路径**:Agent 可能找到更好的方法
|
||||
5. **平衡问题集**:测试"应该做"和"不应该做"
|
||||
6. **测试隔离**:每个测试使用独立环境
|
||||
7. **阅读 Transcript**:理解 Agent 行为的最佳方式
|
||||
|
||||
### 立即行动
|
||||
|
||||
- [ ] 为现有工具添加单元测试
|
||||
- [ ] 创建 Mock Provider 用于测试
|
||||
- [ ] 设计 3-5 个核心能力测试
|
||||
- [ ] 设置 CI 自动运行测试
|
||||
- [ ] 建立 Transcript 审查习惯
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Anthropic: Demystifying evals for AI agents](https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents)
|
||||
- [Rust Testing Guide](https://doc.rust-lang.org/book/ch11-00-testing.html)
|
||||
- [Tokio Testing](https://tokio.rs/tokio/topics/testing)
|
||||
|
||||
---
|
||||
|
||||
*本文档定义了 ProxyCast AI Agent 功能的测试策略,随着开发进展会持续更新。*
|
||||
Reference in New Issue
Block a user