Skip to main content

lellm_core/
prompt.rs

1//! 分层 Prompt — 统一 System Prompt 表示,最大化前缀缓存命中率。
2//!
3//! # 设计目标
4//!
5//! - **统一概念**:`Prompt` 是唯一类型,无论简单文本还是分层结构
6//! - **零迁移成本**:`impl From<String>` 让旧代码无缝升级
7//! - **前缀缓存**:每层独立 `cache_control`,稳定性高的层永远命中
8//!
9//! # 缓存层级(稳定性递减)
10//!
11//! | 层级 | 内容 | 变化频率 | 缓存收益 |
12//! |------|------|---------|---------|
13//! | L1 | 核心身份 | 永不 | 最高 |
14//! | L2 | 工具指南 | 极少 | 高 |
15//! | L3 | 项目规则 | 偶尔 | 中 |
16//! | L4 | 注入记忆 | 每轮 | 低 |
17//! | L5 | 会话上下文 | 频繁 | 最低(通常不缓存)|
18
19use crate::{CacheControl, ContentBlock};
20
21/// Prompt 层 — 一段文本 + 可选的缓存控制标记。
22///
23/// 内部使用,不对外暴露。用户通过 `PromptBuilder` 操作。
24#[derive(Debug, Clone)]
25struct PromptLayer {
26    text: String,
27    cache_control: Option<CacheControl>,
28}
29
30/// 统一的 Prompt 表示。
31///
32/// 内部始终为分层结构,即使是简单文本也会转换为单层。
33/// 这保证了 API 的一致性——无论用户传入 `&str` 还是 `PromptBuilder` 的结果,
34/// 框架内部处理路径完全相同。
35///
36/// # 示例
37///
38/// ```
39/// use lellm_core::{Prompt, PromptBuilder, CacheControl};
40///
41/// // 简单文本 — 自动转换
42/// let simple: Prompt = "You are a helpful assistant.".into();
43///
44/// // 分层构建 — 最大化前缀缓存
45/// let layered = Prompt::builder()
46///     .layer_cached("核心身份…")               // 永不变化
47///     .layer_cached("工具指南…")               // 极少变化
48///     .layer_dynamic("会话上下文: …")          // 每轮变化
49///     .build();
50///
51/// // 合并为纯文本(用于不支持 cache_control 的 Provider)
52/// let text = layered.build_text();
53/// ```
54#[derive(Debug, Clone)]
55pub struct Prompt {
56    layers: Vec<PromptLayer>,
57}
58
59impl Prompt {
60    /// 从纯文本创建 Prompt(单层,无缓存标记)。
61    pub fn plain(text: impl Into<String>) -> Self {
62        Self {
63            layers: vec![PromptLayer {
64                text: text.into(),
65                cache_control: None,
66            }],
67        }
68    }
69
70    /// 创建分层构建器。
71    pub fn builder() -> PromptBuilder {
72        PromptBuilder::new()
73    }
74
75    /// 将 Prompt 转换为带 cache_control 的 `Vec<ContentBlock>`。
76    ///
77    /// 供框架内部构建 `Message::System` 使用。
78    pub fn to_content_blocks(&self) -> Vec<ContentBlock> {
79        self.layers
80            .iter()
81            .map(|layer| match layer.cache_control {
82                Some(cache) => ContentBlock::text_with_cache(layer.text.clone(), cache),
83                None => ContentBlock::text(&layer.text),
84            })
85            .collect()
86    }
87
88    /// 合并所有层为纯文本,层之间以 `\n\n` 分隔。
89    ///
90    /// 用于不支持 `cache_control` 的 Provider(如 OpenAI、Google)。
91    pub fn build_text(&self) -> String {
92        self.layers
93            .iter()
94            .map(|layer| layer.text.clone())
95            .collect::<Vec<_>>()
96            .join("\n\n")
97    }
98
99    /// 是否为空(无层或所有层为空)。
100    pub fn is_empty(&self) -> bool {
101        self.layers.iter().all(|l| l.text.is_empty())
102    }
103}
104
105impl From<String> for Prompt {
106    fn from(s: String) -> Self {
107        Self::plain(s)
108    }
109}
110
111impl From<&str> for Prompt {
112    fn from(s: &str) -> Self {
113        Self::plain(s)
114    }
115}
116
117impl From<&String> for Prompt {
118    fn from(s: &String) -> Self {
119        Self::plain(s.clone())
120    }
121}
122
123impl Default for Prompt {
124    fn default() -> Self {
125        Self { layers: vec![] }
126    }
127}
128
129/// Prompt 分层构建器。
130///
131/// 每一层可以独立设置缓存策略,最大化前缀缓存命中率。
132///
133/// # 示例
134///
135/// ```
136/// use lellm_core::{Prompt, PromptBuilder, CacheControl};
137///
138/// let prompt = Prompt::builder()
139///     // L1 — 核心身份,永不变化 → 永远命中缓存
140///     .layer_cached("你是 DevOps Agent,专注于 CI/CD 管理。")
141///     // L2 — 工具指南,极少变化 → 长期命中缓存
142///     .layer_cached("可用工具: get_time, get_env, get_config")
143///     // L3 — 项目规则,偶尔变化
144///     .layer_cached("项目规则: 使用中文回复。")
145///     // L4 — 分隔符
146///     .layer_cached("---")
147///     // L5 — 注入记忆,每轮变化
148///     .layer_cached("相关记忆: 用户偏好 Jenkins。")
149///     // L6 — 会话上下文,频繁变化 → 不缓存
150///     .layer_dynamic("当前目标: 部署 ds-pkg")
151///     .build();
152/// ```
153#[derive(Debug, Clone, Default)]
154pub struct PromptBuilder {
155    layers: Vec<PromptLayer>,
156}
157
158impl PromptBuilder {
159    pub fn new() -> Self {
160        Self::default()
161    }
162
163    /// 添加带 `CacheControl::Breakpoint` 缓存断点的层。
164    ///
165    /// 用于稳定性高的内容(核心身份、工具指南、项目规则)。
166    /// 这是最常用的方法——绝大多数层都应该缓存。
167    pub fn layer_cached(mut self, text: impl Into<String>) -> Self {
168        self.layers.push(PromptLayer {
169            text: text.into(),
170            cache_control: Some(CacheControl::Breakpoint),
171        });
172        self
173    }
174
175    /// 添加不带缓存的层。
176    ///
177    /// 用于频繁变化的内容(会话上下文、临时注入信息)。
178    pub fn layer_dynamic(mut self, text: impl Into<String>) -> Self {
179        self.layers.push(PromptLayer {
180            text: text.into(),
181            cache_control: None,
182        });
183        self
184    }
185
186    /// 添加带自定义缓存策略的层。
187    ///
188    /// 当前只有 `CacheControl::Breakpoint`,预留未来扩展。
189    pub fn layer(mut self, text: impl Into<String>, cache: Option<CacheControl>) -> Self {
190        self.layers.push(PromptLayer {
191            text: text.into(),
192            cache_control: cache,
193        });
194        self
195    }
196
197    /// 构建为 `Prompt`。
198    pub fn build(self) -> Prompt {
199        Prompt {
200            layers: self.layers,
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_prompt_from_string() {
211        let prompt: Prompt = "hello".into();
212        let blocks = prompt.to_content_blocks();
213        assert_eq!(blocks.len(), 1);
214        assert_eq!(blocks[0].as_text(), Some("hello"));
215        if let ContentBlock::Text(t) = &blocks[0] {
216            assert!(t.cache_control.is_none());
217        }
218    }
219
220    #[test]
221    fn test_prompt_from_str() {
222        let s = "world";
223        let prompt: Prompt = s.into();
224        assert_eq!(prompt.build_text(), "world");
225    }
226
227    #[test]
228    fn test_prompt_plain() {
229        let prompt = Prompt::plain("plain text");
230        assert_eq!(prompt.build_text(), "plain text");
231        let blocks = prompt.to_content_blocks();
232        assert_eq!(blocks.len(), 1);
233        assert!(matches!(
234            &blocks[0],
235            ContentBlock::Text(t) if t.cache_control.is_none()
236        ));
237    }
238
239    #[test]
240    fn test_prompt_builder_layered() {
241        let prompt = Prompt::builder()
242            .layer_cached("layer1")
243            .layer_cached("layer2")
244            .layer_dynamic("dynamic")
245            .build();
246
247        let blocks = prompt.to_content_blocks();
248        assert_eq!(blocks.len(), 3);
249        assert_eq!(blocks.len(), 3);
250
251        // Layer 1 — cached
252        if let ContentBlock::Text(t) = &blocks[0] {
253            assert_eq!(t.text, "layer1");
254            assert!(t.cache_control.is_some());
255        } else {
256            panic!("expected Text block");
257        }
258
259        // Layer 2 — cached
260        if let ContentBlock::Text(t) = &blocks[1] {
261            assert_eq!(t.text, "layer2");
262            assert!(t.cache_control.is_some());
263        } else {
264            panic!("expected Text block");
265        }
266
267        // Layer 3 — dynamic (no cache)
268        if let ContentBlock::Text(t) = &blocks[2] {
269            assert_eq!(t.text, "dynamic");
270            assert!(t.cache_control.is_none());
271        } else {
272            panic!("expected Text block");
273        }
274    }
275
276    #[test]
277    fn test_build_text_joins_with_double_newline() {
278        let prompt = Prompt::builder()
279            .layer_cached("A")
280            .layer_cached("B")
281            .build();
282
283        assert_eq!(prompt.build_text(), "A\n\nB");
284    }
285
286    #[test]
287    fn test_build_text_single_layer() {
288        let prompt = Prompt::builder().layer_cached("only").build();
289
290        assert_eq!(prompt.build_text(), "only");
291    }
292
293    #[test]
294    fn test_prompt_is_empty() {
295        let empty = Prompt::default();
296        assert!(empty.is_empty());
297
298        let nonempty: Prompt = "x".into();
299        assert!(!nonempty.is_empty());
300    }
301
302    #[test]
303    fn test_prompt_layer_custom_cache() {
304        let prompt = Prompt::builder()
305            .layer("cached", Some(CacheControl::Breakpoint))
306            .layer("no cache", None)
307            .build();
308
309        let blocks = prompt.to_content_blocks();
310        if let ContentBlock::Text(t) = &blocks[0] {
311            assert!(t.cache_control.is_some());
312        } else {
313            panic!("expected Text");
314        }
315        if let ContentBlock::Text(t) = &blocks[1] {
316            assert!(t.cache_control.is_none());
317        } else {
318            panic!("expected Text");
319        }
320    }
321
322    #[test]
323    fn test_empty_layers_produce_empty_blocks() {
324        let prompt = Prompt::builder().build();
325        assert!(prompt.to_content_blocks().is_empty());
326        assert_eq!(prompt.build_text(), "");
327    }
328}