5.4 自定义上下文提供器
自定义 `IContextProvider` 是 RAF 推荐的扩展 Agent 行为能力的方式。本章通过两个完整示例展示构建自定义提供器的步骤和最佳实践。
基础模板
use async_trait::async_trait;
use rust_agent_core::{
ContextResult, IAgent, IContextProvider, ISession, ChatMessage,
AgentRunOptions, AgentResponse, AgentError, Result,
};
pub struct MyContextProvider {
// 提供器的配置和状态
config: MyConfig,
}
#[async_trait]
impl IContextProvider for MyContextProvider {
fn name(&self) -> &str {
"MyContextProvider"
}
fn kind(&self) -> &str {
"custom"
}
async fn on_invoking(
&self,
_agent: &dyn IAgent,
_session: &dyn ISession,
_messages: &[ChatMessage],
_options: &AgentRunOptions,
) -> Result<ContextResult> {
// 注入指令、消息或工具
Ok(ContextResult::default())
}
async fn on_invoked(
&self,
_agent: &dyn IAgent,
_session: &dyn ISession,
_request_messages: &[ChatMessage],
_response: Option<&AgentResponse>,
_error: Option<&AgentError>,
) -> Result<()> {
// 后置处理:持久化、日志等
Ok(())
}
}
示例一:用户画像上下文注入
这个提供器在每个 Agent 调用前查询用户画像,将用户偏好注入 system prompt。
use async_trait::async_trait;
use rust_agent_core::{
AgentResponse, AgentRunOptions, ChatMessage, ContextResult,
IAgent, IContextProvider, ISession, ProviderState, Result,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
/// 用户画像数据结构
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct UserProfile {
name: String,
preferred_language: String,
expertise_level: String,
timezone: String,
}
/// 将用户画像注入 Agent 上下文的提供器
pub struct UserProfileProvider {
/// 用户 ID(可从请求上下文、JWT token 等获取)
user_id: String,
/// 异步获取画像的回调
profile_fetcher: Arc<dyn Fn(&str) -> Result<UserProfile> + Send + Sync>,
}
impl UserProfileProvider {
pub fn new(
user_id: impl Into<String>,
fetcher: Arc<dyn Fn(&str) -> Result<UserProfile> + Send + Sync>,
) -> Self {
Self {
user_id: user_id.into(),
profile_fetcher: fetcher,
}
}
fn build_instructions(&self, profile: &UserProfile) -> String {
format!(
"## 当前用户信息\n\
- 姓名: {name}\n\
- 偏好语言: {lang}\n\
- 技能水平: {level}\n\
- 时区: {tz}\n\n\
请根据用户的技能水平调整回答的详细程度,\
使用 {lang} 回复,并注意时区 {tz} 的时间相关计算。",
name = profile.name,
lang = profile.preferred_language,
level = profile.expertise_level,
tz = profile.timezone,
)
}
}
#[async_trait]
impl IContextProvider for UserProfileProvider {
fn name(&self) -> &str {
"UserProfileProvider"
}
fn kind(&self) -> &str {
"custom"
}
async fn on_invoking(
&self,
_agent: &dyn IAgent,
session: &dyn ISession,
_messages: &[ChatMessage],
_options: &AgentRunOptions,
) -> Result<ContextResult> {
// 获取或从 session 缓存中加载用户画像
let profile = {
let state = ProviderState::<UserProfile>::new("UserProfileProvider");
let mut cached = state.get_or_init(session);
// 如果缓存为空或用户 ID 变化,重新获取
if cached.name.is_empty() || cached.name != self.user_id {
match (self.profile_fetcher)(&self.user_id) {
Ok(fresh) => {
cached = fresh;
// 缓存到 session,下次不需要重新查询
let _ = state.save(session, &cached);
}
Err(e) => {
tracing::warn!(error = %e, user_id = %self.user_id,
"Failed to fetch user profile, using defaults");
// 使用默认画像,不阻断 Agent 调用
cached = UserProfile::default();
}
}
}
cached
};
Ok(ContextResult {
instructions: Some(self.build_instructions(&profile)),
..Default::default()
})
}
async fn on_invoked(
&self,
_agent: &dyn IAgent,
_session: &dyn ISession,
_request_messages: &[ChatMessage],
_response: Option<&AgentResponse>,
_error: Option<&AgentError>,
) -> Result<()> {
// 可以记录每次调用的用户行为日志
tracing::info!(
user_id = %self.user_id,
has_error = _error.is_some(),
"Agent invoked for user"
);
Ok(())
}
}
使用示例
use std::sync::Arc;
// 模拟从数据库加载用户画像
let profile_fetcher = Arc::new(|user_id: &str| -> Result<UserProfile> {
// 实际场景中,这里可能是数据库查询或 API 调用
Ok(UserProfile {
name: "张三".into(),
preferred_language: "中文".into(),
expertise_level: "高级".into(),
timezone: "Asia/Shanghai".into(),
})
});
let provider = UserProfileProvider::new("user-001", profile_fetcher);
let agent = AgentBuilder::new()
.with_context_provider(Arc::new(provider))
.build()?;
示例二:数据库查询结果注入
这个提供器在每次 Agent 调用前执行预定义的数据库查询,将查询结果作为上下文注入。
use async_trait::async_trait;
use rust_agent_core::{
AgentResponse, AgentRunOptions, ChatMessage, ContextResult,
IAgent, IContextProvider, ISession, Result,
};
use std::sync::Arc;
/// 预定义的数据库查询提供器
pub struct DatabaseContextProvider {
/// 数据库连接池
pool: sqlx::PgPool,
/// 预定义的查询列表(名称 → SQL)
queries: Vec<(String, String)>,
}
impl DatabaseContextProvider {
pub fn new(pool: sqlx::PgPool, queries: Vec<(String, String)>) -> Self {
Self { pool, queries }
}
async fn execute_queries(&self) -> Result<String> {
let mut context = String::from("## 系统数据快照\n\n");
for (name, sql) in &self.queries {
// 执行查询
let rows: Vec<sqlx::postgres::PgRow> = sqlx::query(sql)
.fetch_all(&self.pool)
.await
.map_err(|e| {
rust_agent_core::AgentError::ConfigError(format!(
"查询 '{}' 执行失败: {}", name, e
))
})?;
// 格式化结果(简化示例;实际可用 serde 序列化)
context.push_str(&format!("### {}\n", name));
if rows.is_empty() {
context.push_str("无数据\n\n");
} else {
context.push_str(&format!("{} 条记录\n\n", rows.len()));
}
}
Ok(context)
}
}
#[async_trait]
impl IContextProvider for DatabaseContextProvider {
fn name(&self) -> &str {
"DatabaseContextProvider"
}
fn kind(&self) -> &str {
"knowledge"
}
async fn on_invoking(
&self,
_agent: &dyn IAgent,
_session: &dyn ISession,
_messages: &[ChatMessage],
_options: &AgentRunOptions,
) -> Result<ContextResult> {
let instructions = match self.execute_queries().await {
Ok(ctx) => Some(ctx),
Err(e) => {
tracing::error!(error = %e, "Failed to execute DB queries");
None
}
};
Ok(ContextResult {
instructions,
..Default::default()
})
}
async fn on_invoked(
&self,
_agent: &dyn IAgent,
_session: &dyn ISession,
_request_messages: &[ChatMessage],
_response: Option<&AgentResponse>,
_error: Option<&AgentError>,
) -> Result<()> {
Ok(())
}
}
使用示例
let queries = vec![
("活跃用户数".into(), "SELECT COUNT(*) FROM users WHERE last_active > NOW() - INTERVAL '1 day'".into()),
("待处理订单".into(), "SELECT COUNT(*) FROM orders WHERE status = 'pending'".into()),
("系统健康状态".into(), "SELECT status FROM system_health ORDER BY checked_at DESC LIMIT 1".into()),
];
let provider = DatabaseContextProvider::new(pg_pool, queries);
let agent = AgentBuilder::new()
.with_context_provider(Arc::new(provider))
.build()?;
示例三:消息计数限制提供器
确保 LLM 接收的消息不超过指定数量,超出时自动压缩:
use async_trait::async_trait;
use rust_agent_core::{
AgentResponse, AgentRunOptions, ChatMessage, ContextResult,
IAgent, IContextProvider, ISession, Result,
};
/// 限制注入消息数量的提供器
pub struct MessageLimitProvider {
/// 最大消息数(超过后使用 replace_messages)
max_messages: usize,
/// 保留最近的消息数
keep_recent: usize,
}
impl MessageLimitProvider {
pub fn new(max_messages: usize, keep_recent: usize) -> Self {
Self { max_messages, keep_recent }
}
}
#[async_trait]
impl IContextProvider for MessageLimitProvider {
fn name(&self) -> &str {
"MessageLimitProvider"
}
fn kind(&self) -> &str {
"custom"
}
async fn on_invoking(
&self,
_agent: &dyn IAgent,
_session: &dyn ISession,
messages: &[ChatMessage],
_options: &AgentRunOptions,
) -> Result<ContextResult> {
// 消息数在阈值内 → 不干扰
if messages.len() <= self.max_messages {
return Ok(ContextResult::default());
}
// 超出阈值 → 只保留最近 N 条消息
let start = messages.len().saturating_sub(self.keep_recent);
let truncated: Vec<ChatMessage> = messages[start..].to_vec();
Ok(ContextResult {
instructions: Some(format!(
"[上下文压缩] 原始 {} 条消息,截断至最近 {} 条。",
messages.len(),
truncated.len()
)),
messages: truncated,
replace_messages: true, // ← 关键:替换前面的消息列表
..Default::default()
})
}
async fn on_invoked(
&self, _agent: &dyn IAgent, _session: &dyn ISession,
_request_messages: &[ChatMessage],
_response: Option<&AgentResponse>, _error: Option<&AgentError>,
) -> Result<()> {
Ok(())
}
}
提供器排序策略
由于提供器按注册顺序执行,合理安排排序很重要:
let agent = AgentBuilder::new()
// 1. 先加载历史(所有消息的基础)
.with_context_provider(Arc::new(InMemoryHistoryProvider::new()))
// 2. 注入技能和工具
.with_context_provider(Arc::new(AgentSkillsProvider::scan("./skills")?))
// 3. 注入工作区信息
.with_context_provider(Arc::new(workspace_provider))
// 4. 注入用户画像(靠后,个性化信息优先显示)
.with_context_provider(Arc::new(user_profile_provider))
// 5. 最后压缩(限制消息总数)
.with_context_provider(Arc::new(MessageLimitProvider::new(50, 20)))
.build()?;
**排序原则:**
1. 数据加载型提供器靠前(历史、状态等) 2. 工具注入型提供器居中(技能、工作区) 3. 个性化/指令型提供器靠后(用户画像、业务规则) 4. 压缩/截断型提供器最后(`replace_messages = true`)
最佳实践
1. **错误不应阻断 Agent 调用**——`on_invoking` 失败时返回 `ContextResult::default()` 比抛出错误更好 2. **使用 `ProviderState` 缓存数据**——避免每次 `on_invoking` 都查询数据库 3. **`on_invoked` 中的 `error` 传参**——可用于失败告警或状态回滚 4. **命名清晰**——`name()` 返回的字符串应能唯一标识提供器 5. **`replace_messages` 谨慎使用**——确保不会误删其他提供器注入的重要消息