1use lc_schema::Message;
9use serde::de::DeserializeOwned;
10
11use crate::language_models::BaseChatModel;
12use crate::tools::ToolDefinition;
13
14#[derive(Debug, thiserror::Error)]
18pub enum StructuredJudgeError {
19 #[error("LLM 调用失败: {0}")]
21 Call(String),
22 #[error("结构化解析失败: {0}")]
24 Parse(String),
25}
26
27pub async fn structured_call<M, T, F>(
38 judge: &M,
39 tool: ToolDefinition,
40 messages: Vec<Message>,
41 text_fallback: F,
42) -> Result<T, StructuredJudgeError>
43where
44 M: BaseChatModel,
45 T: DeserializeOwned,
46 F: FnOnce(&str) -> Result<T, StructuredJudgeError>,
47{
48 if let Some(bound) = judge.bind_tools(vec![tool]) {
49 let result = bound
50 .chat(messages, None)
51 .await
52 .map_err(|e| StructuredJudgeError::Call(e.to_string()))?;
53 match result.tool_calls {
54 Some(calls) => {
55 let call = calls.first().ok_or_else(|| {
56 StructuredJudgeError::Parse("裁判返回的 tool_calls 为空".to_string())
57 })?;
58 let parsed = call.parse_arguments::<T>().map_err(|e| {
59 StructuredJudgeError::Parse(format!("裁判结构化参数解析失败: {}", e))
60 })?;
61 Ok(parsed)
62 }
63 None => {
64 log::warn!("裁判模型绑定了工具但返回纯文本,回落文本解析");
65 text_fallback(&result.content)
66 }
67 }
68 } else {
69 log::warn!("裁判模型不支持 bind_tools,回落文本解析");
70 let result = judge
71 .chat(messages, None)
72 .await
73 .map_err(|e| StructuredJudgeError::Call(e.to_string()))?;
74 text_fallback(&result.content)
75 }
76}
77
78pub fn truncate(s: &str, max: usize) -> String {
80 if s.chars().count() <= max {
81 s.to_string()
82 } else {
83 let truncated: String = s.chars().take(max).collect();
84 format!("{}...", truncated)
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use async_trait::async_trait;
92 use futures_util::Stream;
93 use lc_schema::Message;
94 use std::pin::Pin;
95 use std::sync::atomic::{AtomicUsize, Ordering};
96 use std::sync::Arc;
97
98 use crate::language_models::LLMResult;
99 use crate::{BaseLanguageModel, Runnable, RunnableConfig};
100
101 #[derive(Debug)]
102 struct JudgeError(String);
103 impl std::fmt::Display for JudgeError {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 write!(f, "{}", self.0)
106 }
107 }
108 impl std::error::Error for JudgeError {}
109
110 struct SeqMockJudge {
112 replies: Vec<String>,
113 call: Arc<AtomicUsize>,
114 }
115 impl SeqMockJudge {
116 fn new(replies: Vec<String>) -> Self {
117 Self {
118 replies,
119 call: Arc::new(AtomicUsize::new(0)),
120 }
121 }
122 }
123
124 #[async_trait]
125 impl Runnable<Vec<Message>, LLMResult> for SeqMockJudge {
126 type Error = JudgeError;
127 async fn invoke(
128 &self,
129 _input: Vec<Message>,
130 _config: Option<RunnableConfig>,
131 ) -> Result<LLMResult, Self::Error> {
132 Err(JudgeError("use chat".into()))
133 }
134 }
135
136 #[async_trait]
137 impl BaseLanguageModel<Vec<Message>, LLMResult> for SeqMockJudge {
138 fn model_name(&self) -> &str {
139 "seq-mock"
140 }
141 fn get_num_tokens(&self, t: &str) -> usize {
142 t.len()
143 }
144 fn with_temperature(self, _: f32) -> Self {
145 self
146 }
147 fn with_max_tokens(self, _: usize) -> Self {
148 self
149 }
150 }
151
152 #[async_trait]
153 impl BaseChatModel for SeqMockJudge {
154 async fn chat(
155 &self,
156 _messages: Vec<Message>,
157 _config: Option<RunnableConfig>,
158 ) -> Result<LLMResult, Self::Error> {
159 let idx = self.call.fetch_add(1, Ordering::SeqCst);
160 let reply = self.replies.get(idx).cloned().unwrap_or_default();
161 Ok(LLMResult {
162 content: reply,
163 model: "seq-mock".to_string(),
164 token_usage: None,
165 tool_calls: None,
166 thinking_content: None,
167 })
168 }
169 async fn stream_chat(
170 &self,
171 _messages: Vec<Message>,
172 _config: Option<RunnableConfig>,
173 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, Self::Error>> + Send>>, Self::Error>
174 {
175 Err(JudgeError("not supported".into()))
176 }
177 }
178
179 #[derive(serde::Deserialize, Debug)]
180 struct MockArgs {
181 verdict: String,
182 }
183
184 fn mock_tool() -> ToolDefinition {
185 ToolDefinition::new("mock_judge", "返回判定。")
186 }
187
188 #[tokio::test]
189 async fn test_fallback_on_text_only_model() {
190 let judge = SeqMockJudge::new(vec!["yes".into()]);
192 let messages = vec![Message::human("判断")];
193 let out = structured_call(&judge, mock_tool(), messages, |raw| {
194 Ok(MockArgs {
195 verdict: raw.trim().to_string(),
196 })
197 })
198 .await
199 .unwrap();
200 assert_eq!(out.verdict, "yes");
201 }
202
203 #[tokio::test]
204 async fn test_parse_error_raised_not_silently_defaulted() {
205 let judge = SeqMockJudge::new(vec!["没法判断".into()]);
207 let messages = vec![Message::human("判断")];
208 let err = structured_call(
209 &judge,
210 mock_tool(),
211 messages,
212 |_raw: &str| -> Result<MockArgs, StructuredJudgeError> {
213 Err(StructuredJudgeError::Parse("解析失败".into()))
214 },
215 )
216 .await
217 .unwrap_err();
218 assert!(matches!(err, StructuredJudgeError::Parse(_)));
219 }
220}