Skip to main content

lc_core/output_parsers/
structured_parser.rs

1use async_trait::async_trait;
2use futures_util::Stream;
3use serde::de::DeserializeOwned;
4use std::collections::HashMap;
5use std::marker::PhantomData;
6use std::pin::Pin;
7
8use super::base::{BaseOutputParser, OutputParserError, OutputParserResult};
9use crate::language_models::LLMResult;
10use crate::runnables::{Runnable, RunnableConfig};
11
12/// 结构化输出解析器
13///
14/// 将 LLM 输出的键值对格式(每行一个 `key: value`)解析为 HashMap。
15/// 适用于 LLM 以非 JSON 格式输出结构化信息的场景。
16///
17/// # 格式
18/// 输入格式应为每行一个 `key: value`,例如:
19/// ```text
20/// 姓名: 张三
21/// 年龄: 28
22/// 城市: 北京
23/// ```
24///
25/// # 示例
26/// ```ignore
27/// use langchainrust::output_parsers::StructuredOutputParser;
28///
29/// let parser = StructuredOutputParser::new();
30/// let result = parser.parse("姓名: 张三\n年龄: 28").await?;
31/// assert_eq!(result.get("姓名").unwrap(), "张三");
32/// ```
33pub struct StructuredOutputParser {
34    /// 键值对之间的分隔符
35    separator: char,
36}
37
38impl StructuredOutputParser {
39    pub fn new() -> Self {
40        Self { separator: ':' }
41    }
42
43    /// 使用自定义分隔符创建解析器
44    pub fn with_separator(separator: char) -> Self {
45        Self { separator }
46    }
47}
48
49impl Default for StructuredOutputParser {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55#[async_trait]
56impl BaseOutputParser<HashMap<String, String>> for StructuredOutputParser {
57    async fn parse(&self, text: &str) -> OutputParserResult<HashMap<String, String>> {
58        let mut map = HashMap::new();
59
60        for line in text.lines() {
61            let line = line.trim();
62            if line.is_empty() {
63                continue;
64            }
65
66            if let Some(pos) = line.find(self.separator) {
67                // `pos` 是分隔符首字节的字节索引;多字节分隔符(如全角 `:`)时
68                // pos+1 会落在字符内部导致切片 panic,须按分隔符 UTF-8 宽度跳过
69                let sep_len = self.separator.len_utf8();
70                let key = line[..pos].trim().to_string();
71                let value = line[pos + sep_len..].trim().to_string();
72
73                if !key.is_empty() {
74                    map.insert(key, value);
75                }
76            }
77        }
78
79        Ok(map)
80    }
81
82    fn get_format_instructions(&self) -> String {
83        format!(
84            "请按以下格式输出(每行一个键值对,使用 '{}' 分隔):\n键{}值",
85            self.separator, self.separator
86        )
87    }
88}
89
90#[async_trait]
91impl Runnable<LLMResult, HashMap<String, String>> for StructuredOutputParser {
92    type Error = OutputParserError;
93
94    async fn invoke(
95        &self,
96        input: LLMResult,
97        _config: Option<RunnableConfig>,
98    ) -> Result<HashMap<String, String>, Self::Error> {
99        self.parse(&input.content).await
100    }
101
102    async fn stream(
103        &self,
104        input: LLMResult,
105        _config: Option<RunnableConfig>,
106    ) -> Result<
107        Pin<Box<dyn Stream<Item = Result<HashMap<String, String>, Self::Error>> + Send>>,
108        Self::Error,
109    > {
110        let result = self.parse(&input.content).await?;
111        let stream = futures_util::stream::once(async move { Ok(result) });
112        Ok(Box::pin(stream))
113    }
114}
115
116/// 类型化输出解析器
117///
118/// 将 LLM 输出的 JSON 字符串解析为指定的 Rust 结构体。
119/// 相当于 Python LangChain 的 `PydanticOutputParser`(使用 serde 替代 pydantic)。
120///
121/// 需要目标类型实现 `serde::Deserialize`。
122///
123/// # 示例
124/// ```ignore
125/// use serde::Deserialize;
126/// use langchainrust::output_parsers::TypedOutputParser;
127///
128/// #[derive(Deserialize, Debug, PartialEq)]
129/// struct Person {
130///     name: String,
131///     age: u32,
132/// }
133///
134/// let parser = TypedOutputParser::<Person>::new();
135/// let person = parser.parse(r#"{"name": "Alice", "age": 30}"#).await?;
136/// assert_eq!(person.name, "Alice");
137/// ```
138pub struct TypedOutputParser<T> {
139    _phantom: PhantomData<T>,
140}
141
142impl<T> TypedOutputParser<T> {
143    pub fn new() -> Self {
144        Self {
145            _phantom: PhantomData,
146        }
147    }
148}
149
150impl<T: DeserializeOwned> Default for TypedOutputParser<T> {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156#[async_trait]
157impl<T: DeserializeOwned + Send + Sync + 'static> BaseOutputParser<T> for TypedOutputParser<T> {
158    async fn parse(&self, text: &str) -> OutputParserResult<T> {
159        let text = text.trim();
160
161        // 尝试从 Markdown 代码块中提取 JSON
162        let json_str = Self::extract_from_markdown(text).unwrap_or(text);
163
164        // 先尝试解析为 Value 验证合法性
165        serde_json::from_str::<serde_json::Value>(json_str)
166            .map_err(|e| OutputParserError::JsonError(format!("输入不是合法 JSON:{}", e)))?;
167
168        // 反序列化为目标类型
169        serde_json::from_str::<T>(json_str).map_err(|e| {
170            OutputParserError::TypeError(format!(
171                "类型反序列化失败(请检查 JSON 字段是否匹配):{}",
172                e
173            ))
174        })
175    }
176}
177
178impl<T: DeserializeOwned> TypedOutputParser<T> {
179    /// 从 Markdown 代码块中提取 JSON 字符串
180    fn extract_from_markdown(text: &str) -> Option<&str> {
181        // 尝试 ```json ... ```
182        if let Some(start) = text.find("```json") {
183            let after = &text[start + 7..];
184            if let Some(end) = after.find("```") {
185                return Some(after[..end].trim());
186            }
187        }
188        // 尝试 ``` ... ```
189        if let Some(start) = text.find("```") {
190            let after = &text[start + 3..];
191            let after = after.trim();
192            let skip = after.find('\n').unwrap_or(0);
193            let after = &after[skip..].trim();
194            if let Some(end) = after.find("```") {
195                return Some(after[..end].trim());
196            }
197        }
198        None
199    }
200}
201
202#[async_trait]
203impl<T: DeserializeOwned + Send + Sync + 'static> Runnable<LLMResult, T> for TypedOutputParser<T> {
204    type Error = OutputParserError;
205
206    async fn invoke(
207        &self,
208        input: LLMResult,
209        _config: Option<RunnableConfig>,
210    ) -> Result<T, Self::Error> {
211        self.parse(&input.content).await
212    }
213
214    async fn stream(
215        &self,
216        input: LLMResult,
217        _config: Option<RunnableConfig>,
218    ) -> Result<Pin<Box<dyn Stream<Item = Result<T, Self::Error>> + Send>>, Self::Error> {
219        let result = self.parse(&input.content).await?;
220        let stream = futures_util::stream::once(async move { Ok(result) });
221        Ok(Box::pin(stream))
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[tokio::test]
230    async fn test_structured_parser_default_colon() {
231        let parser = StructuredOutputParser::new();
232        let map = parser.parse("姓名: 张三\n年龄: 28").await.unwrap();
233        assert_eq!(map.get("姓名").unwrap(), "张三");
234        assert_eq!(map.get("年龄").unwrap(), "28");
235    }
236
237    #[tokio::test]
238    async fn test_structured_parser_fullwidth_separator() {
239        // 全角冒号 3 字节:修复前按 pos+1 切片会切在字符内部 panic
240        let parser = StructuredOutputParser::with_separator(':');
241        let map = parser.parse("姓名:张三\n年龄:28").await.unwrap();
242        assert_eq!(map.get("姓名").unwrap(), "张三");
243        assert_eq!(map.get("年龄").unwrap(), "28");
244    }
245
246    #[tokio::test]
247    async fn test_structured_parser_runnable_invoke() {
248        let parser = StructuredOutputParser::new();
249        let map = parser
250            .invoke(
251                LLMResult {
252                    content: "状态: 成功".to_string(),
253                    ..Default::default()
254                },
255                None,
256            )
257            .await
258            .unwrap();
259        assert_eq!(map.get("状态").unwrap(), "成功");
260    }
261}