diff --git a/docs/develop/ai-agent-development-guide.md b/docs/develop/ai-agent-development-guide.md new file mode 100644 index 000000000..39c043df4 --- /dev/null +++ b/docs/develop/ai-agent-development-guide.md @@ -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, + /// 行为约束 + 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; + + /// 工具名称 + 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, + /// 工具执行器 + tools: HashMap>, + /// 对话历史 + messages: Vec, + /// 运行状态 + state: AgentState, +} + +impl AgentRuntime { + /// 执行 Agent 循环 + pub async fn run(&mut self, user_input: &str) -> Result { + 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 }, + Assistant { content: Vec, tool_calls: Option> }, + 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; +} + +/// 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, +} + +impl StreamProcessor { + /// 处理流式数据块 + pub fn process_chunk(&mut self, chunk: &str) -> Vec { + 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>, +} + +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) { + 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, + /// 禁止的命令 + pub blocked_commands: Vec, + /// 允许的 HTTP 域名 + pub allowed_domains: Vec, + /// 最大文件大小 + 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((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 功能的架构设计,随着开发进展会持续更新。* diff --git a/docs/tests/ai-agent-testing-guide.md b/docs/tests/ai-agent-testing-guide.md new file mode 100644 index 000000000..424c0cb21 --- /dev/null +++ b/docs/tests/ai-agent-testing-guide.md @@ -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, +} + +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, +} + +/// 人工评分收集器 +pub struct HumanGradeCollector { + grades: Vec, +} + +impl HumanGradeCollector { + /// 计算评分者一致性 + pub fn inter_rater_agreement(&self) -> f64 { + // 计算 Cohen's Kappa 或 Fleiss' Kappa + todo!() + } + + /// 导出用于校准模型评分器 + pub fn export_for_calibration(&self) -> Vec { + 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, +} + +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 { + 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, +} + +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 { + 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 功能的测试策略,随着开发进展会持续更新。*