agent_context/context/types.rs
1//! 后端 trait 定义。
2//!
3//! [`ContextBackend`] 封装 LLM 后端的消息工厂、格式转换、模型对话和配置信息。
4//! [`ContextBackendResponse`] 约束后端 Response 类型,提供统一的响应访问接口。
5
6use crate::error::AgentError;
7use crate::message::ContextMessage;
8
9// ---------------------------------------------------------------------------
10// ContextBackendResponse — Response 类型约束
11// ---------------------------------------------------------------------------
12
13/// 工具调用信息。从后端 Response 中提取,供 consumer 执行工具并构造 Tool 角色消息。
14#[derive(Debug, Clone)]
15pub struct ToolCallInfo {
16 /// 工具调用唯一标识,对应 [`ContextBackend::tool_message`] 的 `tool_call_id`。
17 pub id: String,
18 /// 函数名。
19 pub name: String,
20 /// 函数参数(JSON 字符串)。
21 pub arguments: String,
22}
23
24/// 后端 Response 类型约束,流式/非流式 Response 均需实现。
25///
26/// 提供:
27/// - [`response_type`](Self::response_type):内容分类,供 [`ContextBackend::classify_chunk`] 默认实现
28/// - [`reasoning_content`](Self::reasoning_content):思维链文本
29/// - [`content`](Self::content):正文文本
30/// - [`tool_calls`](Self::tool_calls):工具调用信息
31pub trait ContextBackendResponse {
32 /// 返回响应包含的内容类型。
33 fn response_type(&self) -> ResponseType;
34
35 /// 提取思维链文本(流式为 delta,非流式为完整内容)。
36 ///
37 /// `None` 表示字段不存在/null,`Some("")` 表示空字符串。
38 fn reasoning_content(&self) -> Option<String>;
39
40 /// 提取正文文本(流式为 delta,非流式为完整内容)。
41 ///
42 /// `None` 表示字段不存在/null,`Some("")` 表示空字符串。
43 fn content(&self) -> Option<String>;
44
45 /// 提取工具调用信息(流式为 delta,非流式为完整列表)。
46 fn tool_calls(&self) -> Vec<ToolCallInfo>;
47}
48
49/// 响应内容类型枚举。
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum ResponseType {
52 /// 无增量内容
53 Empty,
54 /// 仅思维链
55 Reasoning,
56 /// 仅正文
57 Content,
58 /// 同时包含思维链和正文
59 ReasoningAndContent,
60}
61
62// ---------------------------------------------------------------------------
63// 流式输出事件
64// ---------------------------------------------------------------------------
65
66/// 流式输出事件,由 [`ContextBackend::classify_chunk`] 产出。
67///
68/// 每个事件持有原始后端响应引用,用户可从中提取任意数据(content、reasoning、usage 等)。
69/// 事件类型仅做阶段标记,不做数据裁剪。
70#[derive(Debug, Clone)]
71pub enum StreamEvent<R> {
72 /// 思维链增量响应
73 Thinking(R),
74 /// 第一个正文增量响应(思维链→正文的过渡点)
75 ContentFirst(R),
76 /// 后续正文增量响应
77 Content(R),
78 /// 工具调用增量响应
79 ToolCalls(R),
80}
81
82// ---------------------------------------------------------------------------
83// CommonOpts — 请求级公共配置
84// ---------------------------------------------------------------------------
85
86/// 请求级公共配置,内嵌于各后端的 Opts 类型。
87///
88/// [`ContextBackend::Opts`] 通过 `AsRef<CommonOpts>` 约束,确保后端 Opts 提供这些字段。
89/// 每次请求由调用方显式构造,不提供 `Default`。
90#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct CommonOpts {
92 /// 模型标识(如 `"deepseek-v4-pro"`、`"glm-4-flash"`)。
93 pub model: String,
94 /// 模型上下文窗口大小(token 数),用于自动压缩检测。
95 pub context_window: usize,
96 /// 最大输出 token 数。
97 pub max_tokens: usize,
98 /// 上下文溢出时是否自动压缩。`false` 时溢出返回错误。
99 pub auto_compress: bool,
100 /// 每轮刷新的临时元数据,发送时作为 system 消息拼接到对话末尾,不存储。
101 pub scratch: Option<String>,
102}
103
104// ---------------------------------------------------------------------------
105// ContextBackend trait
106// ---------------------------------------------------------------------------
107
108/// 后端 trait:抽象 LLM 后端的完整接口。
109///
110/// 实现此 trait 即可让 [`AgentContext`](crate::AgentContext) 对接任意 LLM 后端(DeepSeek、智谱、OpenAI 等)。
111///
112/// ## 方法分类
113///
114/// | 类别 | 方法 | 类型 |
115/// |------|------|------|
116/// | 消息工厂 | [`user_message`](Self::user_message)、[`system_message`](Self::system_message)、[`tool_message`](Self::tool_message) | 实例方法 |
117/// | 格式转换 | [`to_system_message`](Self::to_system_message)、[`to_request_messages`](Self::to_request_messages) | 实例方法(默认实现) |
118/// | 响应解析 | [`extract_messages`](Self::extract_messages) | 实例方法 |
119/// | 模型对话 | [`estimate_tokens`](Self::estimate_tokens)、[`send`](Self::send)、[`send_stream`](Self::send_stream) | 实例方法 |
120pub trait ContextBackend: Send + Sync + Clone + 'static {
121 /// 后端消息类型,必须实现 [`ContextMessage`]。
122 type Message: ContextMessage;
123 /// 后端自定义的请求选项类型,须内嵌 [`CommonOpts`] 并实现 `AsRef<CommonOpts>`。
124 ///
125 /// 典型用途:传递 `model`、`temperature`、`thinking` 等模型参数。
126 type Opts: AsRef<CommonOpts> + Clone + Send + Sync;
127 /// 后端完整的 API 响应类型。
128 ///
129 /// - 非流式:ChatCompletion(含 choices + usage)
130 /// - 流式:ChatCompletionChunk(含 delta content / reasoning_content)
131 type Response: Clone + Send + Sync + ContextBackendResponse;
132
133 // 消息工厂(实例方法)
134 /// 构造一条 User 角色消息。
135 fn user_message(&self, content: impl Into<String> + Send) -> Self::Message;
136 /// 构造一条 System 角色消息。
137 fn system_message(&self, content: impl Into<String> + Send) -> Self::Message;
138 /// 构造一条 Tool 角色消息(工具调用结果)。
139 fn tool_message(
140 &self,
141 tool_call_id: impl Into<String> + Send,
142 content: impl Into<String> + Send,
143 ) -> Self::Message;
144
145 // 格式转换(实例方法,含默认实现)
146 /// 将消息转换为 System 角色(用于压缩摘要等场景)。
147 ///
148 /// 默认实现调用 [`ContextMessage::with_role`]。
149 fn to_system_message(&self, msg: Self::Message) -> Self::Message {
150 msg.with_role(crate::Role::System)
151 }
152
153 /// 将后端响应消息转换为请求格式。
154 ///
155 /// 对 `!preserve_reasoning()` 的消息剥离 `reasoning_content`,减少网络传输和 token 消耗。
156 ///
157 /// 默认实现调用 [`ContextMessage::without_reasoning`]。
158 fn to_request_messages(
159 &self,
160 messages: Vec<Self::Message>,
161 ) -> Result<Vec<Self::Message>, AgentError> {
162 Ok(messages
163 .into_iter()
164 .map(|m| {
165 if m.preserve_reasoning() {
166 m
167 } else {
168 m.without_reasoning()
169 }
170 })
171 .collect())
172 }
173
174 // 响应解析(实例方法)
175 /// 将流式分块合并为单条消息。
176 ///
177 /// 累加 `content`、`reasoning_content`、`tool_calls`,构造完整的 assistant 消息。
178 /// 返回 `None` 表示分块中没有有效数据。
179 fn merge_chunks(&self, responses: &[Self::Response]) -> Option<Self::Message>;
180
181 /// 从后端非流式响应中提取消息列表。流式场景请用 [`merge_chunks`](Self::merge_chunks)。
182 fn extract_messages(
183 &self,
184 responses: &[Self::Response],
185 ) -> Result<Vec<Self::Message>, AgentError>;
186
187 // 模型对话(实例方法)
188 /// 估算消息列表的 token 数量。I/O 操作(可能需要调用远程 tokenizer API)。
189 fn estimate_tokens(
190 &self,
191 messages: &[Self::Message],
192 ) -> impl std::future::Future<Output = Result<usize, AgentError>> + Send;
193
194 /// 非流式对话。发送全部消息,返回完整 Response(含 usage 等元数据)。
195 fn send(
196 &self,
197 messages: &[Self::Message],
198 opts: &Self::Opts,
199 ) -> impl std::future::Future<Output = Result<Self::Response, AgentError>> + Send;
200
201 /// 流式对话。参数为 owned(数据已移动),返回 `'static` 流。
202 fn send_stream(
203 &self,
204 messages: Vec<Self::Message>,
205 opts: Self::Opts,
206 ) -> impl futures_core::Stream<Item = Result<Self::Response, AgentError>> + Send + 'static;
207
208 /// 将流式分块分类为结构化事件,同时更新阶段状态。
209 ///
210 /// 默认实现基于 [`ContextBackendResponse::response_type`] 判断阶段:
211 /// - 含 `reasoning_content` → [`StreamEvent::Thinking`]
212 /// - 第一个 `content`(且之前有思维链)→ [`StreamEvent::ContentFirst`]
213 /// - 后续 `content` → [`StreamEvent::Content`]
214 fn classify_chunk(
215 &self,
216 response: &Self::Response,
217 saw_thinking: &mut bool,
218 ) -> Vec<StreamEvent<Self::Response>> {
219 let mut events = Vec::new();
220 if !response.tool_calls().is_empty() {
221 events.push(StreamEvent::ToolCalls(response.clone()));
222 }
223 match response.response_type() {
224 ResponseType::Empty => return events,
225 ResponseType::Reasoning => {
226 events.push(StreamEvent::Thinking(response.clone()));
227 *saw_thinking = true;
228 }
229 ResponseType::Content => {
230 if *saw_thinking {
231 events.push(StreamEvent::ContentFirst(response.clone()));
232 *saw_thinking = false;
233 } else {
234 events.push(StreamEvent::Content(response.clone()));
235 }
236 }
237 ResponseType::ReasoningAndContent => {
238 events.push(StreamEvent::Thinking(response.clone()));
239 *saw_thinking = true;
240 events.push(StreamEvent::ContentFirst(response.clone()));
241 *saw_thinking = false;
242 }
243 }
244 events
245 }
246
247 /// 将消息序列化为紧凑 JSON 字符串。
248 fn message_to_json(&self, msg: &Self::Message) -> Result<String, AgentError> {
249 serde_json::to_string(msg).map_err(|e| AgentError::Context(e.to_string()))
250 }
251
252 /// 从 JSON 字符串反序列化为消息。
253 fn message_from_json(&self, line: &str) -> Result<Self::Message, AgentError> {
254 serde_json::from_str(line).map_err(|e| AgentError::Context(format!("JSON 解析失败: {e}")))
255 }
256}