From bc9537cd80e13501741df5dd6f4a00d657377a65 Mon Sep 17 00:00:00 2001 From: iven Date: Wed, 22 Apr 2026 14:23:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(hands):=20hand=5Fresearcher=20=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AE=B9=E9=94=99=20=E2=80=94=20LLM=E4=B8=8D=E4=BC=A0?= =?UTF-8?q?action=E5=AD=97=E6=AE=B5=E6=97=B6=E8=87=AA=E5=8A=A8=E6=8E=A8?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: glm等国内LLM调用hand_researcher时不带action字段, 导致"missing field action"反复报错触发LoopGuard拦截。 修复: execute()先尝试严格反序列化,失败时调用infer_action() 从输入字段推断意图: - 有query → search - 有url → fetch - 有urls → summarize - 都没有 → 友好错误提示 验证: 160 tests PASS --- crates/zclaw-hands/src/hands/researcher.rs | 62 +++++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/crates/zclaw-hands/src/hands/researcher.rs b/crates/zclaw-hands/src/hands/researcher.rs index 742c910..35dbcf3 100644 --- a/crates/zclaw-hands/src/hands/researcher.rs +++ b/crates/zclaw-hands/src/hands/researcher.rs @@ -301,6 +301,61 @@ impl ResearcherHand { } } + /// Infer action from input fields when LLM omits the `action` field. + /// Many LLMs (especially non-OpenAI models like glm) call tools without + /// including the enum tag, e.g. sending `{"query": "search terms"}` instead + /// of `{"action": "search", "query": "search terms"}`. + fn infer_action(input: &Value) -> Result { + // Has "url" (singular) → fetch + if let Some(url) = input.get("url").and_then(|v| v.as_str()) { + if !url.is_empty() && url.starts_with("http") { + return Ok(ResearcherAction::Fetch { url: url.to_string() }); + } + } + // Has "urls" (plural) → summarize + if let Some(urls) = input.get("urls").and_then(|v| v.as_array()) { + let url_list: Vec = urls.iter() + .filter_map(|v| v.as_str().map(|s| s.to_string())) + .collect(); + if !url_list.is_empty() { + return Ok(ResearcherAction::Summarize { urls: url_list }); + } + } + // Has "query" (string or object) → search + if let Some(query_val) = input.get("query") { + let query: ResearchQuery = if query_val.is_string() { + // LLM sent plain string: {"query": "search terms"} + ResearchQuery { + query: query_val.as_str().unwrap_or("").to_string(), + engine: SearchEngine::Auto, + depth: ResearchDepth::Standard, + max_results: 10, + include_related: false, + time_limit_secs: 60, + } + } else { + // LLM sent object: {"query": {"query": "...", "engine": "..."}} + serde_json::from_value(query_val.clone()).unwrap_or_else(|_| ResearchQuery { + query: query_val.get("query") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + engine: SearchEngine::Auto, + depth: ResearchDepth::Standard, + max_results: 10, + include_related: false, + time_limit_secs: 60, + }) + }; + if !query.query.trim().is_empty() { + return Ok(ResearcherAction::Search { query }); + } + } + Err(zclaw_types::ZclawError::HandError( + "无法识别搜索意图:请提供 query(搜索)或 url(获取网页)参数".to_string() + )) + } + /// Execute a web search — route to the configured backend async fn execute_search(&self, query: &ResearchQuery) -> Result> { query.validate().map_err(|e| zclaw_types::ZclawError::HandError(e))?; @@ -1030,8 +1085,11 @@ impl Hand for ResearcherHand { } async fn execute(&self, _context: &HandContext, input: Value) -> Result { - let action: ResearcherAction = serde_json::from_value(input.clone()) - .map_err(|e| zclaw_types::ZclawError::HandError(format!("Invalid action: {}", e)))?; + // Try strict deserialization first, then fall back to inference + let action: ResearcherAction = match serde_json::from_value(input.clone()) { + Ok(a) => a, + Err(_) => Self::infer_action(&input)?, + }; let start = std::time::Instant::now();