1. 项目概述
在AI技术快速发展的今天,Agent(智能代理)已成为一个热门话题。与传统的单次问答式AI不同,Agent能够自主思考、调用工具、拆解任务并持续执行,更接近人类的工作方式。大多数AI Agent教程都使用Python实现,但在需要高并发、可控资源和长期稳定运行的场景中(如爬取、自动化运营、链上监控等),Rust语言展现出独特优势。
本文将带你从零开始用Rust构建一个功能完整的AI Agent,它具有以下核心特性:
- 完整的Plan→Act→Observe循环机制
- 灵活的工具调用(Tool Calling)能力
- 短期记忆(对话上下文)和长期记忆(本地存储/向量库接口)系统
- 并发执行工具、限流和重试机制
- 支持任意LLM提供商的接入
2. 为什么选择Rust构建AI Agent?
2.1 Rust vs Python的性能对比
在AI Agent开发中,Rust相比Python有几个关键优势:
-
性能优势:Rust编译为原生代码,无GC开销,特别适合高并发场景。实测显示,在相同硬件条件下,Rust实现的工具调用吞吐量可达Python的3-5倍。
-
内存安全:Rust的所有权系统从根本上避免了内存泄漏和数据竞争问题,这对于需要长期稳定运行的Agent至关重要。
-
可观测性:Rust的tracing生态系统(tracing-subscriber、tokio-console等)提供了强大的运行时诊断能力。
-
部署简便:Rust可编译为静态链接的二进制文件,部署时无需担心依赖问题。
2.2 适用场景分析
Rust实现的AI Agent特别适合以下场景:
- 高吞吐任务编排(大量工具调用、IO密集型操作)
- 需要7×24小时稳定运行的自动化系统
- 对资源使用效率敏感的生产环境
- 作为基础设施组件长期迭代演进
3. 核心架构设计
3.1 Agent的核心组件
一个实用的AI Agent通常包含以下核心部件:
- LLM(大脑):负责推理和生成行动指令
- Tools(手脚):执行具体操作,如HTTP请求、数据库查询等
- Memory(记忆):保存历史信息,避免"鱼类记忆"
- Loop(循环):执行"规划-行动-观察"的循环过程
- Safety & Guardrails(护栏):防止无限循环、越权操作等风险
3.2 项目目录结构
建议采用以下模块化目录结构:
code复制agent-rs/
src/
main.rs
agent/mod.rs
agent/loop.rs
llm/mod.rs
tools/mod.rs
tools/http.rs
tools/fs.rs
memory/mod.rs
memory/short_term.rs
memory/long_term.rs
types.rs
这种设计的核心思想是:
- Agent不直接依赖特定模型厂商(llm模块可替换)
- 工具通过trait抽象(可无限扩展新工具)
- 记忆系统可插拔(支持本地文件、SQLite、Redis等多种存储)
4. 核心数据结构定义
4.1 消息与工具规范
首先定义Agent内部通信的基本数据结构:
rust复制// src/types.rs
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: String, // "system" | "user" | "assistant" | "tool"
pub content: String,
pub name: Option<String>, // tool name if role == "tool"
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSpec {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value, // JSON Schema
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AgentAction {
ToolCall {
tool_name: String,
input: serde_json::Value,
},
Final {
answer: String,
},
}
这种设计采用简单的JSON约定,让LLM输出结构化数据,解析为AgentAction。虽然许多厂商支持原生tool calling,但JSON输出更加通用。
5. 工具系统实现
5.1 工具trait抽象
使用async_trait定义工具的基本接口:
rust复制// src/tools/mod.rs
use async_trait::async_trait;
use serde_json::Value;
use anyhow::Result;
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn spec(&self) -> crate::types::ToolSpec;
async fn call(&self, input: Value) -> Result<Value>;
}
pub struct ToolRegistry {
tools: std::collections::HashMap<String, std::sync::Arc<dyn Tool>>,
}
impl ToolRegistry {
pub fn new() -> Self { Self { tools: Default::default() } }
pub fn register<T: Tool + 'static>(&mut self, tool: T) {
self.tools.insert(tool.name().to_string(), std::sync::Arc::new(tool));
}
pub fn list_specs(&self) -> Vec<crate::types::ToolSpec> {
self.tools.values().map(|t| t.spec()).collect()
}
pub fn get(&self, name: &str) -> Option<std::sync::Arc<dyn Tool>> {
self.tools.get(name).cloned()
}
}
5.2 具体工具实现示例
HTTP GET工具:
rust复制// src/tools/http.rs
use async_trait::async_trait;
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
pub struct HttpGetTool;
#[async_trait]
impl crate::tools::Tool for HttpGetTool {
fn name(&self) -> &str { "http_get" }
fn spec(&self) -> crate::types::ToolSpec {
crate::types::ToolSpec {
name: self.name().into(),
description: "Send HTTP GET request and return response text".into(),
input_schema: json!({
"type": "object",
"properties": {
"url": {"type":"string"}
},
"required": ["url"]
}),
}
}
async fn call(&self, input: Value) -> Result<Value> {
let url = input.get("url").and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing url"))?;
let resp = reqwest::get(url).await?.text().await?;
Ok(json!({ "text": resp }))
}
}
文件读取工具:
rust复制// src/tools/fs.rs
use async_trait::async_trait;
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
pub struct ReadFileTool;
#[async_trait]
impl crate::tools::Tool for ReadFileTool {
fn name(&self) -> &str { "read_file" }
fn spec(&self) -> crate::types::ToolSpec {
crate::types::ToolSpec {
name: self.name().into(),
description: "Read a local text file (UTF-8)".into(),
input_schema: json!({
"type":"object",
"properties": { "path": {"type":"string"} },
"required":["path"]
}),
}
}
async fn call(&self, input: Value) -> Result<Value> {
let path = input.get("path").and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing path"))?;
let text = tokio::fs::read_to_string(path).await?;
Ok(json!({ "text": text }))
}
}
6. 记忆系统设计
6.1 短期记忆实现
短期记忆主要用于维护对话上下文,需要注意窗口裁剪以避免token爆炸:
rust复制// src/memory/short_term.rs
use crate::types::ChatMessage;
pub struct ShortTermMemory {
pub messages: Vec<ChatMessage>,
pub max_messages: usize,
}
impl ShortTermMemory {
pub fn new(max_messages: usize) -> Self {
Self { messages: vec![], max_messages }
}
pub fn push(&mut self, msg: ChatMessage) {
self.messages.push(msg);
if self.messages.len() > self.max_messages {
let overflow = self.messages.len() - self.max_messages;
self.messages.drain(0..overflow);
}
}
pub fn all(&self) -> &[ChatMessage] {
&self.messages
}
}
6.2 长期记忆实现
长期记忆先实现一个简单的本地JSONL追加写(后续可替换为SQLite/向量库):
rust复制// src/memory/long_term.rs
use anyhow::Result;
use serde_json::Value;
use tokio::io::AsyncWriteExt;
pub struct LongTermMemory {
path: String,
}
impl LongTermMemory {
pub fn new(path: impl Into<String>) -> Self {
Self { path: path.into() }
}
pub async fn append_event(&self, event: &Value) -> Result<()> {
let mut f = tokio::fs::OpenOptions::new()
.create(true).append(true)
.open(&self.path).await?;
f.write_all(event.to_string().as_bytes()).await?;
f.write_all(b"\n").await?;
Ok(())
}
}
7. LLM客户端抽象
7.1 LLM trait设计
通过trait隔离不同厂商的实现差异:
rust复制// src/llm/mod.rs
use async_trait::async_trait;
use anyhow::Result;
use crate::types::{ChatMessage, ToolSpec};
#[async_trait]
pub trait LlmClient: Send + Sync {
async fn complete(
&self,
system_prompt: &str,
messages: &[ChatMessage],
tools: &[ToolSpec],
) -> Result<String>;
}
你可以根据需要实现OpenAIClient、AnthropicClient或LocalModelClient等具体实现。本文重点在Agent架构,故不展开厂商细节。
8. Agent核心循环实现
8.1 Plan→Act→Observe循环
这是Agent的核心逻辑,让LLM每轮输出严格JSON:
rust复制// src/agent/loop.rs
use anyhow::{Result, anyhow};
use serde_json::Value;
use crate::types::{AgentAction, ChatMessage};
pub struct AgentLoop<L: crate::llm::LlmClient> {
pub llm: std::sync::Arc<L>,
pub tools: crate::tools::ToolRegistry,
pub short_memory: crate::memory::short_term::ShortTermMemory,
pub long_memory: crate::memory::long_term::LongTermMemory,
pub system_prompt: String,
pub max_steps: usize,
}
impl<L: crate::llm::LlmClient> AgentLoop<L> {
pub async fn run(&mut self, user_goal: &str) -> Result<String> {
self.short_memory.push(ChatMessage {
role: "user".into(),
content: user_goal.into(),
name: None,
});
for step in 0..self.max_steps {
let tool_specs = self.tools.list_specs();
let raw = self.llm
.complete(&self.system_prompt, self.short_memory.all(), &tool_specs)
.await?;
let action: AgentAction = serde_json::from_str(&raw)
.map_err(|e| anyhow!("LLM output is not valid AgentAction JSON: {e}. raw={raw}"))?;
match action {
AgentAction::Final { answer } => {
self.long_memory.append_event(&serde_json::json!({
"type":"final",
"step": step,
"answer": answer
})).await?;
return Ok(answer);
}
AgentAction::ToolCall { tool_name, input } => {
let tool = self.tools.get(&tool_name)
.ok_or_else(|| anyhow!("tool not found: {tool_name}"))?;
let out = tool.call(input).await?;
self.short_memory.push(ChatMessage {
role: "tool".into(),
name: Some(tool_name.clone()),
content: out.to_string(),
});
self.long_memory.append_event(&serde_json::json!({
"type":"tool_result",
"step": step,
"tool": tool_name,
"output": out
})).await?;
}
}
}
Err(anyhow!("max_steps reached without Final"))
}
}
8.2 System Prompt设计
好的system prompt需要:
- 约束输出格式(必须JSON)
- 说明何时调用工具、何时结束
- 强调工具输入要符合schema
- 防止无限循环
示例:
rust复制const SYSTEM_PROMPT: &str = r#"You are a helpful AI agent.
You MUST respond in valid JSON that matches one of:
1) {"type":"ToolCall","tool_name": "...", "input": {...}}
2) {"type":"Final","answer":"..."}
Rules:
- Use tools when you need external data.
- Tool input MUST follow the tool's JSON schema.
- If you have enough information, end with Final.
- If repeated tool calls do not improve progress, summarize and Final."#;
9. 主程序组装
9.1 组件初始化与运行
将所有组件组装起来并运行:
rust复制// src/main.rs
mod types;
mod llm;
mod tools;
mod memory;
mod agent;
use tools::{ToolRegistry};
use tools::http::HttpGetTool;
use tools::fs::ReadFileTool;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 1) LLM client(需自行实现)
let llm = std::sync::Arc::new(MyLlmClient::new_from_env()?);
// 2) 工具注册
let mut registry = ToolRegistry::new();
registry.register(HttpGetTool);
registry.register(ReadFileTool);
// 3) 记忆系统
let short_memory = memory::short_term::ShortTermMemory::new(30);
let long_memory = memory::long_term::LongTermMemory::new("agent_events.jsonl");
// 4) Agent循环
let mut agent = agent::loop_::AgentLoop {
llm,
tools: registry,
short_memory,
long_memory,
system_prompt: SYSTEM_PROMPT.into(),
max_steps: 20,
};
let goal = "Read ./README.md and summarize it, then fetch https://example.com and compare topics.";
let answer = agent.run(goal).await?;
println!("{answer}");
Ok(())
}
10. 工程化建议
10.1 生产环境关键考量
写出MVP不难,难的是"上线跑一个月不崩"。以下是关键点:
工具调用增强
- 超时控制:为reqwest配置合理超时
- 重试机制:使用tokio-retry或实现指数退避
- 限流保护:使用governor等库控制并发量
并发工具执行
当LLM规划多个独立动作时,可使用tokio::join!或FuturesUnordered并发执行。注意合并observation并保持可追溯性。
可观测性
- 使用tracing + tracing-subscriber记录日志
- 为每次tool call创建span,记录工具名、延迟、负载大小和错误
安全护栏
- 设置max_steps限制
- 检测"重复调用同一工具且结果相似"的情况并强制Final
- 对高风险工具(写文件/转账等)实施allowlist和人工确认机制
记忆优化
- 采用事件流(JSONL) + 摘要(定期压缩)的方式
- 需要时再检索(RAG/向量库),而非全量塞入prompt
11. Rust Agent适用场景总结
Rust实现的AI Agent特别适合以下场景:
- 高吞吐任务编排(大量工具调用、IO密集)
- 需要长期稳定运行的自动化系统
- 对资源使用效率敏感的环境
- 作为基础设施组件长期迭代演进
相比Python方案,Rust版本在性能、稳定性和资源控制方面有明显优势,虽然开发效率略低,但在生产环境中这些优势往往更为关键。