多模态 AI Agent 的 Rust SDK 设计文本、图像与代码的统一处理一、多模态 Agent 的工程挑战三种输入三套处理逻辑早期的 AI Agent 只处理文本——用户发一句话Agent 回复一段文字。但实际业务中用户可能上传一张截图问这个报错怎么修或者贴一段代码问这里有什么 Bug。多模态 Agent 需要同时处理文本、图像和代码三种输入每种输入的预处理、编码和推理逻辑完全不同。文本输入需要分词Tokenization和嵌入Embedding图像输入需要缩放、归一化和视觉编码Vision Encoding代码输入需要语法解析和结构化表示。如果每种输入都写一套独立的处理逻辑Agent 的代码会变成三个互不关联的模块——新增一种模态就要重写一遍集成逻辑。Rust SDK 的设计目标是用统一的接口抽象多模态输入让 Agent 的核心逻辑不关心输入是文本、图像还是代码。具体做法是定义Modalitytrait每种模态实现自己的编码逻辑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[流式响应处理] end2.2 Modality Trait 设计Modalitytrait 是整个 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) - ResultEncodedInput, 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) - ResultEncodedInput, 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: OptionString, } #[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) - ResultEncodedInput, 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: OptionString, } #[async_trait] impl Modality for CodeInput { fn modality_type(self) - ModalityType { ModalityType::Code { language: self.language.clone(), } } async fn encode(self) - ResultEncodedInput, 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: VecBoxdyn Modality, system_prompt: OptionString, max_tokens: Optionu32, temperature: Optionf32, } 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) - ResultLlmRequest, 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: OptionString, pub user_prompt: String, pub images: VecImageData, pub max_tokens: Optionu32, pub temperature: Optionf32, } 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) - ResultString, Boxdyn 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 的运行时开销VecBoxdyn Modality使用动态分发每次调用encode()和to_prompt_segment()都需要虚表查找。在模态数量少通常 10的场景下这个开销可以忽略。但如果需要处理大量输入如批量图像分析可以考虑用枚举替代 trait object获得静态分发的性能优势。4.3 适用边界多模态 Agent SDK 适用于以下场景Agent 需要处理多种输入类型、输入类型在运行时才能确定、需要兼容不同 LLM API纯文本和多模态。不适用于只处理单一模态不需要抽象层、对性能要求极高trait object 的间接调用开销不可接受、输入类型固定且已知用枚举更简洁。五、总结多模态 Agent SDK 的核心是Modalitytrait——用统一的接口抽象不同输入类型的编码逻辑。核心落地步骤如下定义 Modality trait统一encode()和to_prompt_segment()接口各模态独立实现。实现三种模态TextInput直接传递、ImageInputBase64 编码、CodeInput代码块格式化。构建 Agent 请求通过 Builder 模式组合多种模态输入自动拼接 prompt。兼容多模态 API图像数据以image_url格式传递纯文本 LLM 自动降级为文本描述。扩展新模态只需实现Modalitytrait不需要修改 Agent 核心逻辑。抽象的目标不是消除差异而是让差异不影响核心逻辑。多模态 Agent SDK 让处理什么输入和如何推理解耦新增一种模态只需要加一个 trait 实现。