多模态 AI Agent 的 Rust SDK 设计:文本、图像与代码的统一处理
多模态 AI Agent 的 Rust SDK 设计:文本、图像与代码的统一处理

一、多模态 Agent 的工程挑战:三种输入,三套处理逻辑
早期的 AI Agent 只处理文本——用户发一句话,Agent 回复一段文字。但实际业务中,用户可能上传一张截图问"这个报错怎么修",或者贴一段代码问"这里有什么 Bug"。多模态 Agent 需要同时处理文本、图像和代码三种输入,每种输入的预处理、编码和推理逻辑完全不同。
文本输入需要分词(Tokenization)和嵌入(Embedding);图像输入需要缩放、归一化和视觉编码(Vision Encoding);代码输入需要语法解析和结构化表示。如果每种输入都写一套独立的处理逻辑,Agent 的代码会变成三个互不关联的模块——新增一种模态就要重写一遍集成逻辑。
Rust SDK 的设计目标是:用统一的接口抽象多模态输入,让 Agent 的核心逻辑不关心输入是文本、图像还是代码。具体做法是定义 Modality trait,每种模态实现自己的编码逻辑,Agent 通过 trait object 统一调用。
二、多模态 Agent SDK 的架构设计
2.1 分层架构
SDK 分为三层:输入层(Input)、编码层(Encoding)和推理层(Inference)。
flowchart TD
A[用户输入] --> B{模态识别}
B -->|文本| C[TextInput]
B -->|图像| D[ImageInput]
B -->|代码| E[CodeInput]
C --> F[Modality trait]
D --> F
E --> F
F --> G[encode → Embedding]
G --> H[多模态融合]
H --> I[LLM 推理]
I --> J[输出解析]
J --> K[Agent 响应]
subgraph 编码层
L[TextEncoder: Tokenize + Embed]
M[ImageEncoder: Resize + Vision Encode]
N[CodeEncoder: Parse + Structure Encode]
end
subgraph 推理层
O[Prompt 组装]
P[API 调用]
Q[流式响应处理]
end
2.2 Modality Trait 设计
Modality trait 是整个 SDK 的核心抽象。每种模态需要实现两个方法:
encode():将原始输入转换为 LLM 可以理解的格式(文本 prompt 或 embedding 向量)。to_prompt_segment():将编码结果转换为 prompt 片段,拼接到最终的 LLM 请求中。
2.3 多模态融合策略
多模态融合有两种策略:
- 早期融合:在编码阶段将不同模态的 embedding 拼接为一个向量,输入 LLM。需要多模态模型(如 GPT-4V)支持。
- 晚期融合:各模态独立编码,在 prompt 层面拼接为文本描述,输入纯文本 LLM。兼容性更好,但信息损失较大。
SDK 默认采用晚期融合策略(兼容纯文本 LLM),同时提供早期融合的接口供多模态模型使用。
三、Rust 生产级代码实现
3.1 Modality Trait 与模态实现
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// 模态类型标识
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ModalityType {
Text,
Image { format: String },
Code { language: String },
}
/// 编码结果
#[derive(Debug, Clone)]
pub enum EncodedInput {
/// 文本 prompt 片段
TextSegment(String),
/// Base64 编码的图像数据(用于多模态 API)
ImageBase64 { data: String, media_type: String },
/// 结构化代码表示
CodeBlock { language: String, content: String },
}
/// 模态 trait:所有输入类型必须实现
#[async_trait]
pub trait Modality: Send + Sync {
/// 模态类型标识
fn modality_type(&self) -> ModalityType;
/// 编码输入
async fn encode(&self) -> Result<EncodedInput, ModalityError>;
/// 转换为 prompt 片段
fn to_prompt_segment(&self, encoded: &EncodedInput) -> String;
}
/// 模态处理错误
#[derive(Debug, thiserror::Error)]
pub enum ModalityError {
#[error("图像处理失败: {0}")]
ImageProcessing(String),
#[error("代码解析失败: {0}")]
CodeParsing(String),
#[error("编码失败: {0}")]
Encoding(String),
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error),
}
/// 文本输入
pub struct TextInput {
pub content: String,
}
#[async_trait]
impl Modality for TextInput {
fn modality_type(&self) -> ModalityType {
ModalityType::Text
}
async fn encode(&self) -> Result<EncodedInput, ModalityError> {
Ok(EncodedInput::TextSegment(self.content.clone()))
}
fn to_prompt_segment(&self, encoded: &EncodedInput) -> String {
match encoded {
EncodedInput::TextSegment(text) => text.clone(),
_ => unreachable!(),
}
}
}
/// 图像输入
pub struct ImageInput {
pub path: PathBuf,
pub description: Option<String>,
}
#[async_trait]
impl Modality for ImageInput {
fn modality_type(&self) -> ModalityType {
let ext = self.path.extension()
.and_then(|e| e.to_str())
.unwrap_or("png")
.to_string();
ModalityType::Image { format: ext }
}
async fn encode(&self) -> Result<EncodedInput, ModalityError> {
let data = tokio::fs::read(&self.path).await?;
let base64_data = base64::engine::general_purpose::STANDARD.encode(&data);
let media_type = match self.path.extension()
.and_then(|e| e.to_str()) {
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("png") => "image/png",
Some("gif") => "image/gif",
Some("webp") => "image/webp",
_ => "image/png",
};
Ok(EncodedInput::ImageBase64 {
data: base64_data,
media_type: media_type.to_string(),
})
}
fn to_prompt_segment(&self, encoded: &EncodedInput) -> String {
let desc = self.description.as_deref().unwrap_or("图片");
match encoded {
EncodedInput::ImageBase64 { .. } => {
format!("[用户上传了一张{},请分析图片内容]", desc)
}
_ => unreachable!(),
}
}
}
/// 代码输入
pub struct CodeInput {
pub content: String,
pub language: String,
pub context: Option<String>,
}
#[async_trait]
impl Modality for CodeInput {
fn modality_type(&self) -> ModalityType {
ModalityType::Code {
language: self.language.clone(),
}
}
async fn encode(&self) -> Result<EncodedInput, ModalityError> {
Ok(EncodedInput::CodeBlock {
language: self.language.clone(),
content: self.content.clone(),
})
}
fn to_prompt_segment(&self, encoded: &EncodedInput) -> String {
match encoded {
EncodedInput::CodeBlock { language, content } => {
let context = self.context.as_deref().unwrap_or("");
format!(
"```{}\n{}\n```\n{}",
language, content, context
)
}
_ => unreachable!(),
}
}
}
3.2 Agent 请求构建器
/// Agent 请求
pub struct AgentRequest {
inputs: Vec<Box<dyn Modality>>,
system_prompt: Option<String>,
max_tokens: Option<u32>,
temperature: Option<f32>,
}
impl AgentRequest {
pub fn new() -> Self {
Self {
inputs: Vec::new(),
system_prompt: None,
max_tokens: None,
temperature: None,
}
}
/// 添加模态输入
pub fn with_input(mut self, input: impl Modality + 'static) -> Self {
self.inputs.push(Box::new(input));
self
}
pub fn system_prompt(mut self, prompt: &str) -> Self {
self.system_prompt = Some(prompt.to_string());
self
}
pub fn max_tokens(mut self, tokens: u32) -> Self {
self.max_tokens = Some(tokens);
self
}
pub fn temperature(mut self, temp: f32) -> Self {
self.temperature = Some(temp);
self
}
/// 构建最终的 LLM 请求
pub async fn build(self) -> Result<LlmRequest, ModalityError> {
let mut prompt_parts = Vec::new();
let mut images = Vec::new();
for input in &self.inputs {
let encoded = input.encode().await?;
let segment = input.to_prompt_segment(&encoded);
prompt_parts.push(segment);
// 收集图像数据(用于多模态 API)
if let EncodedInput::ImageBase64 { data, media_type } = &encoded {
images.push(ImageData {
data: data.clone(),
media_type: media_type.clone(),
});
}
}
let user_prompt = prompt_parts.join("\n\n");
Ok(LlmRequest {
system_prompt: self.system_prompt,
user_prompt,
images,
max_tokens: self.max_tokens,
temperature: self.temperature,
})
}
}
/// LLM 请求数据
pub struct LlmRequest {
pub system_prompt: Option<String>,
pub user_prompt: String,
pub images: Vec<ImageData>,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
}
pub struct ImageData {
pub data: String,
pub media_type: String,
}
3.3 Agent 运行时
use reqwest::Client;
use serde_json::json;
/// Agent 运行时
pub struct AgentRuntime {
client: Client,
api_base: String,
api_key: String,
model: String,
}
impl AgentRuntime {
pub fn new(api_key: &str, model: &str) -> Self {
Self {
client: Client::new(),
api_base: "https://api.openai.com/v1".to_string(),
api_key: api_key.to_string(),
model: model.to_string(),
}
}
/// 执行 Agent 请求
pub async fn run(&self, request: AgentRequest) -> Result<String, Box<dyn std::error::Error>> {
let llm_request = request.build().await?;
// 构建消息列表
let mut messages = Vec::new();
if let Some(system) = llm_request.system_prompt {
messages.push(json!({
"role": "system",
"content": system,
}));
}
// 如果有图像,使用多模态消息格式
if llm_request.images.is_empty() {
messages.push(json!({
"role": "user",
"content": llm_request.user_prompt,
}));
} else {
let mut content = vec![
json!({ "type": "text", "text": llm_request.user_prompt }),
];
for img in llm_request.images {
content.push(json!({
"type": "image_url",
"image_url": {
"url": format!("data:{};base64,{}", img.media_type, img.data),
}
}));
}
messages.push(json!({
"role": "user",
"content": content,
}));
}
let mut body = json!({
"model": self.model,
"messages": messages,
});
if let Some(max_tokens) = llm_request.max_tokens {
body["max_tokens"] = json!(max_tokens);
}
if let Some(temp) = llm_request.temperature {
body["temperature"] = json!(temp);
}
let response = self.client
.post(format!("{}/chat/completions", self.api_base))
.header("Authorization", format!("Bearer {}", self.api_key))
.json(&body)
.send()
.await?;
let result: serde_json::Value = response.json().await?;
let content = result["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("")
.to_string();
Ok(content)
}
}
四、Trade-offs:多模态 SDK 的设计取舍
4.1 晚期融合的信息损失
晚期融合将图像和代码转换为文本描述,会丢失部分信息。例如,图像中的布局关系和颜色细节很难用文字精确描述。如果业务需要精确的图像理解,应该使用早期融合 + 多模态模型。SDK 的设计允许两种策略共存——encode() 方法返回的 EncodedInput 可以是 embedding 向量(早期融合)或文本描述(晚期融合)。
4.2 trait object 的运行时开销
Vec<Box<dyn Modality>> 使用动态分发,每次调用 encode() 和 to_prompt_segment() 都需要虚表查找。在模态数量少(通常 <10)的场景下,这个开销可以忽略。但如果需要处理大量输入(如批量图像分析),可以考虑用枚举替代 trait object,获得静态分发的性能优势。
4.3 适用边界
多模态 Agent SDK 适用于以下场景:Agent 需要处理多种输入类型、输入类型在运行时才能确定、需要兼容不同 LLM API(纯文本和多模态)。不适用于:只处理单一模态(不需要抽象层)、对性能要求极高(trait object 的间接调用开销不可接受)、输入类型固定且已知(用枚举更简洁)。
五、总结
多模态 Agent SDK 的核心是 Modality trait——用统一的接口抽象不同输入类型的编码逻辑。核心落地步骤如下:
- 定义 Modality trait:统一
encode()和to_prompt_segment()接口,各模态独立实现。 - 实现三种模态:TextInput(直接传递)、ImageInput(Base64 编码)、CodeInput(代码块格式化)。
- 构建 Agent 请求:通过 Builder 模式组合多种模态输入,自动拼接 prompt。
- 兼容多模态 API:图像数据以
image_url格式传递,纯文本 LLM 自动降级为文本描述。 - 扩展新模态:只需实现
Modalitytrait,不需要修改 Agent 核心逻辑。
抽象的目标不是消除差异,而是让差异不影响核心逻辑。多模态 Agent SDK 让"处理什么输入"和"如何推理"解耦,新增一种模态只需要加一个 trait 实现。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)