Skip to main content

lc_agents/react/
agent.rs

1// src/agents/react/agent.rs
2//! ReAct Agent 实现
3//!
4//! 基于 "ReAct: Synergizing Reasoning and Acting in Language Models" 论文。
5//! 支持任何实现了 `BaseChatModel` 的 LLM Provider。
6
7use super::parser::ReActOutputParser;
8use super::prompt::{build_react_prompt, format_scratchpad};
9use crate::{AgentError, AgentOutput, AgentStep, BaseAgent};
10use async_trait::async_trait;
11use lc_core::language_models::BaseChatModel;
12use lc_core::tools::BaseTool;
13use lc_providers::ProviderError;
14use lc_schema::Message;
15use std::collections::HashMap;
16use std::sync::Arc;
17
18/// ReAct Agent
19///
20/// 使用 ReAct (Reasoning + Acting) 模式的 Agent。
21/// 会先思考,然后决定执行什么工具,最后观察结果。
22/// 支持任何实现了 `BaseChatModel` 的 LLM Provider。
23pub struct ReActAgent {
24    /// LLM 客户端
25    llm: Arc<dyn BaseChatModel<Error = ProviderError> + Send + Sync>,
26
27    /// 可用工具列表
28    tools: Vec<Arc<dyn BaseTool>>,
29
30    /// 输出解析器
31    parser: ReActOutputParser,
32
33    /// 自定义系统提示词(可选)
34    system_prompt: Option<String>,
35}
36
37impl ReActAgent {
38    /// 创建新的 ReAct Agent
39    ///
40    /// # 参数
41    /// * `llm` - LLM 客户端(任何实现了 `BaseChatModel` 的类型)
42    /// * `tools` - 可用工具列表
43    /// * `system_prompt` - 自定义系统提示词(可选)
44    ///
45    /// # 向后兼容
46    /// 旧代码 `ReActAgent::new(openai_chat, tools, None)` 仍然可用,
47    /// 因为 `OpenAIChat: BaseChatModel` 且 `OpenAIError: Into<Error>`。
48    pub fn new<L>(llm: L, tools: Vec<Arc<dyn BaseTool>>, system_prompt: Option<String>) -> Self
49    where
50        L: BaseChatModel + Send + Sync + 'static,
51        L::Error: Into<ProviderError>,
52    {
53        Self {
54            llm: lc_providers::wrap_chat_model(llm),
55            tools,
56            parser: ReActOutputParser::new(),
57            system_prompt,
58        }
59    }
60
61    /// 从已包装的 `Arc<dyn BaseChatModel>` 创建 Agent
62    pub fn from_arc(
63        llm: Arc<dyn BaseChatModel<Error = ProviderError> + Send + Sync>,
64        tools: Vec<Arc<dyn BaseTool>>,
65        system_prompt: Option<String>,
66    ) -> Self {
67        Self {
68            llm,
69            tools,
70            parser: ReActOutputParser::new(),
71            system_prompt,
72        }
73    }
74
75    /// 格式化工具描述
76    ///
77    /// 将工具列表格式化为 ReAct prompt 需要的格式
78    fn format_tools(&self) -> String {
79        self.tools
80            .iter()
81            .map(|tool| format!("{}: {}", tool.name(), tool.description()))
82            .collect::<Vec<_>>()
83            .join("\n")
84    }
85
86    /// 获取工具名称列表
87    fn get_tool_names(&self) -> Vec<&str> {
88        self.tools.iter().map(|t| t.name()).collect()
89    }
90
91    /// 构建 ReAct prompt
92    ///
93    /// # 参数
94    /// * `input` - 用户问题
95    /// * `intermediate_steps` - 已执行的步骤历史
96    /// * `history` - 对话历史(可选)
97    fn build_prompt(
98        &self,
99        input: &str,
100        intermediate_steps: &[AgentStep],
101        history: Option<&str>,
102    ) -> String {
103        // 格式化工具描述
104        let tools_description = self.format_tools();
105        let tool_names = self.get_tool_names();
106
107        // 格式化思考历史
108        let scratchpad = format_scratchpad(intermediate_steps);
109
110        // 构建基础 prompt
111        let mut prompt = build_react_prompt(&tools_description, &tool_names, input, &scratchpad);
112
113        // 如果有对话历史,添加到 prompt 开头
114        if let Some(h) = history {
115            if !h.is_empty() {
116                prompt = format!("之前的对话历史:\n{}\n\n{}", h, prompt);
117            }
118        }
119
120        // 如果有自定义系统提示词,添加到 prompt 开头
121        if let Some(sys) = &self.system_prompt {
122            prompt = format!("{}\n\n{}", sys, prompt);
123        }
124
125        prompt
126    }
127}
128
129#[async_trait]
130impl BaseAgent for ReActAgent {
131    /// 规划下一步行动
132    ///
133    /// # 参数
134    /// * `intermediate_steps` - 已执行的步骤历史
135    /// * `inputs` - 用户输入
136    ///
137    /// # 返回
138    /// * `AgentOutput::Action` - 需要执行的动作
139    /// * `AgentOutput::Finish` - 最终答案
140    async fn plan(
141        &self,
142        intermediate_steps: &[AgentStep],
143        inputs: &HashMap<String, String>,
144    ) -> Result<AgentOutput, AgentError> {
145        // 获取用户输入
146        let input = inputs
147            .get("input")
148            .ok_or_else(|| AgentError::Other("Missing input parameter 'input'".to_string()))?;
149
150        // 获取对话历史(如果有)
151        let history = inputs.get("history").map(|s| s.as_str());
152
153        // 构建 prompt
154        let prompt_text = self.build_prompt(input, intermediate_steps, history);
155
156        // 创建消息
157        let messages = vec![Message::human(prompt_text)];
158
159        // 调用 LLM
160        let result = self
161            .llm
162            .chat(messages, None)
163            .await
164            .map_err(|e| AgentError::Other(format!("LLM call failed: {}", e)))?;
165
166        // 解析输出
167        self.parser.parse(&result.content)
168    }
169
170    /// 获取允许的工具列表
171    fn get_allowed_tools(&self) -> Option<Vec<&str>> {
172        Some(self.get_tool_names())
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::{AgentAction, ToolInput};
180    use lc_providers::{OpenAIChat, OpenAIConfig};
181    use lc_tools::Calculator;
182
183    /// 创建测试用的 OpenAI 配置
184    fn create_test_config() -> OpenAIConfig {
185        OpenAIConfig {
186            api_key: "sk-6eb65fcf5d17491ca10b984efe1f43e7".to_string(),
187            base_url:
188                "https://llm-8xo1b7o30z27y2xc.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"
189                    .to_string(),
190            model: "glm-5.2".to_string(),
191            temperature: Some(0.0),
192            max_tokens: Some(500),
193            top_p: None,
194            frequency_penalty: None,
195            presence_penalty: None,
196            streaming: false,
197            organization: None,
198            tools: None,
199            tool_choice: None,
200        }
201    }
202
203    #[test]
204    fn test_format_tools_description() {
205        let config = create_test_config();
206        let llm = OpenAIChat::new(config);
207        let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
208        let agent = ReActAgent::new(llm, tools, None);
209
210        let desc = agent.format_tools();
211        assert!(desc.contains("calculator"));
212    }
213
214    #[test]
215    fn test_get_tool_names() {
216        let config = create_test_config();
217        let llm = OpenAIChat::new(config);
218        let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
219        let agent = ReActAgent::new(llm, tools, None);
220
221        let names = agent.get_tool_names();
222        assert_eq!(names, vec!["calculator"]);
223    }
224
225    #[test]
226    fn test_build_prompt() {
227        let config = create_test_config();
228        let llm = OpenAIChat::new(config);
229        let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
230        let agent = ReActAgent::new(llm, tools, None);
231
232        let prompt = agent.build_prompt("计算 2 + 2", &[], None);
233
234        assert!(prompt.contains("计算 2 + 2"));
235        assert!(prompt.contains("calculator"));
236        assert!(prompt.contains("Question:"));
237        assert!(prompt.contains("Thought:"));
238    }
239
240    #[test]
241    fn test_build_prompt_with_history() {
242        let config = create_test_config();
243        let llm = OpenAIChat::new(config);
244        let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
245        let agent = ReActAgent::new(llm, tools, None);
246
247        let prompt = agent.build_prompt("计算 3 + 3", &[], Some("用户: 你好\n助手: 你好!"));
248
249        assert!(prompt.contains("之前的对话历史"));
250        assert!(prompt.contains("你好"));
251    }
252
253    #[test]
254    fn test_build_prompt_with_system_prompt() {
255        let config = create_test_config();
256        let llm = OpenAIChat::new(config);
257        let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
258        let agent = ReActAgent::new(llm, tools, Some("你是一个数学助手".to_string()));
259
260        let prompt = agent.build_prompt("计算 4 + 4", &[], None);
261
262        assert!(prompt.contains("你是一个数学助手"));
263    }
264
265    /// 真实 API 测试:简单问题(无工具调用)
266    #[tokio::test]
267    #[ignore = "需要真实 API 调用"]
268    async fn test_real_api_simple() {
269        let config = create_test_config();
270        let llm = OpenAIChat::new(config);
271        let tools: Vec<Arc<dyn BaseTool>> = vec![];
272        let agent = ReActAgent::new(llm, tools, None);
273
274        let mut inputs = HashMap::new();
275        inputs.insert("input".to_string(), "什么是 Rust 语言?".to_string());
276
277        let result = agent.plan(&[], &inputs).await.unwrap();
278
279        // 应该直接返回最终答案(因为没有工具)
280        match result {
281            AgentOutput::Finish(finish) => {
282                println!("答案: {:?}", finish.return_values);
283                assert!(finish.output().is_some());
284            }
285            AgentOutput::Action(_) => {
286                println!("LLM 尝试调用工具");
287            }
288            AgentOutput::Actions(_) => {
289                println!("ReActAgent 不支持并行工具调用");
290            }
291        }
292    }
293
294    /// 真实 API 测试:使用计算器
295    #[tokio::test]
296    #[ignore = "需要真实 API 调用"]
297    async fn test_real_api_with_calculator() {
298        let config = create_test_config();
299        let llm = OpenAIChat::new(config);
300        let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
301        let agent = ReActAgent::new(llm, tools, None);
302
303        let mut inputs = HashMap::new();
304        inputs.insert("input".to_string(), "计算 37 加 48 等于多少?".to_string());
305
306        let result = agent.plan(&[], &inputs).await.unwrap();
307
308        match result {
309            AgentOutput::Action(action) => {
310                println!("动作: {}({})", action.tool, action.tool_input);
311                assert_eq!(action.tool, "calculator");
312            }
313            AgentOutput::Finish(finish) => {
314                println!("直接答案: {:?}", finish.return_values);
315            }
316            AgentOutput::Actions(_) => {
317                println!("ReActAgent 不支持并行工具调用");
318            }
319        }
320    }
321
322    /// 真实 API 测试:多步问题
323    #[tokio::test]
324    #[ignore = "需要真实 API 调用"]
325    async fn test_real_api_multi_step() {
326        let config = create_test_config();
327        let llm = OpenAIChat::new(config);
328        let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator)];
329        let agent = ReActAgent::new(llm, tools, None);
330
331        // 创建一个已执行的动作历史
332        let steps = vec![AgentStep::new(
333            AgentAction {
334                tool: "calculator".to_string(),
335                tool_input: ToolInput::String {
336                    value: "37 + 48".to_string(),
337                },
338                log: "我需要先计算 37 + 48".to_string(),
339            },
340            "85".to_string(),
341        )];
342
343        let mut inputs = HashMap::new();
344        inputs.insert(
345            "input".to_string(),
346            "计算 (37 + 48) * 2 等于多少?".to_string(),
347        );
348
349        let result = agent.plan(&steps, &inputs).await.unwrap();
350
351        match result {
352            AgentOutput::Action(action) => {
353                println!("下一步动作: {}({})", action.tool, action.tool_input);
354                assert_eq!(action.tool, "calculator");
355            }
356            AgentOutput::Finish(finish) => {
357                println!("最终答案: {:?}", finish.return_values);
358            }
359            AgentOutput::Actions(_) => {
360                println!("ReActAgent 不支持并行工具调用");
361            }
362        }
363    }
364
365    /// 真实 API 测试:带对话历史
366    #[tokio::test]
367    #[ignore = "需要真实 API 调用"]
368    async fn test_real_api_with_memory() {
369        let config = create_test_config();
370        let llm = OpenAIChat::new(config);
371        let tools: Vec<Arc<dyn BaseTool>> = vec![];
372        let agent = ReActAgent::new(llm, tools, None);
373
374        // 模拟对话历史
375        let history = "Human: 我叫张三\nAI: 好的,张三,我记住了。";
376
377        let mut inputs = HashMap::new();
378        inputs.insert("input".to_string(), "我叫什么名字?".to_string());
379        inputs.insert("history".to_string(), history.to_string());
380
381        let result = agent.plan(&[], &inputs).await.unwrap();
382
383        match result {
384            AgentOutput::Finish(finish) => {
385                println!("答案: {:?}", finish.return_values);
386                let output = finish.output().unwrap_or("");
387                assert!(output.contains("张三"), "应该记住用户名字");
388            }
389            AgentOutput::Action(_) => {
390                println!("LLM 尝试调用工具");
391            }
392            AgentOutput::Actions(_) => {
393                println!("ReActAgent 不支持并行工具调用");
394            }
395        }
396    }
397}