优化: 上下文管理与提示词链路批

- T1 usage 口径: anthropic usage 两层容错+cache 三口径归一(prompt=input+read+creation)
- T2 压缩水位判据: compress_should_trigger 双模式+ratio 钳位,开关关逐字节等价
- T3 跨会话检索: history_search 工具(两跳式)+message_repo 关键词检索+prompt 指引
- T4 代语义闭环: status 终局补写+飞书停靠/换代推卡+交付引导+watcher 兜底补漏
- 段序缓存重排: build_runtime_prompt 恒静段上移/工作上下文压尾(前缀缓存失效面收窄)
- 智谱缓存解析: anthropic 臂补 prompt_tokens_details.cached_tokens 嵌套回退
- 工时禁令: 提示词双语加「禁止预估工时,AI Coding 工时无意义」
- 拖拽文件落盘: 非图片附件存引用行,防整段文本灌入输入框
- 事实修正: 提示词「40+ 工具」→「80+」(基线 81);tool_failure_memory 测试串行锁除竞态
- UI 拆分: TopBar/ToolResultBody 壳层化(top-bar/tool-result-body 子件目录)
This commit is contained in:
lxy
2026-10-26 01:36:00 +08:00
parent 23e505bc40
commit a30f599fe2
59 changed files with 2651 additions and 2541 deletions
+96 -4
View File
@@ -806,12 +806,21 @@ impl LlmProvider for AnthropicCompatProvider {
other => warn!(block_type = other, "Anthropic 响应含未知 content 块类型,已忽略"),
}
}
// 口径归一(G1,P0-A/u-claw 三口径,与 anthropic_helpers message_start 同表):
// anthropic input_tokens 只含未缓存输入,不含 cache_read/creation 两段;
// 落库 prompt_tokens 对齐 openai/deepseek 全量输入语义(hit/miss ⊆ prompt),
// 故 prompt = input + read + creation,total = prompt + output。
// cache 字段:creation(写入)= miss 全价语义,read(命中)= hit 低价。
let prompt_tokens = resp
.usage
.input_tokens
.saturating_add(resp.usage.cache_read())
.saturating_add(resp.usage.cache_creation_input_tokens);
let usage = TokenUsage {
prompt_tokens: resp.usage.input_tokens,
prompt_tokens,
completion_tokens: resp.usage.output_tokens,
total_tokens: resp.usage.input_tokens + resp.usage.output_tokens,
// anthropic cache 字段:creation(写入)= miss 全价语义,read(命中)= hit 低价
prompt_cache_hit_tokens: resp.usage.cache_read_input_tokens,
total_tokens: prompt_tokens.saturating_add(resp.usage.output_tokens),
prompt_cache_hit_tokens: resp.usage.cache_read(),
prompt_cache_miss_tokens: resp.usage.cache_creation_input_tokens,
reasoning_tokens: 0,
};
@@ -1016,6 +1025,47 @@ mod tests {
)
}
/// 辅助:构造带缓存分项的 message_start 事件(cache_read/cache_creation,P0-A 三口径)
fn message_start_with_cache(input_tokens: u32, cache_read: u32, cache_creation: u32) -> String {
format!(
r#"{{"type":"message_start","message":{{"usage":{{"input_tokens":{},"cache_read_input_tokens":{},"cache_creation_input_tokens":{},"output_tokens":0}}}}}}"#,
input_tokens, cache_read, cache_creation
)
}
/// 辅助:构造智谱 anthropic 兼容端点形态的 message_start(隐式缓存命中走嵌套
/// prompt_tokens_details.cached_tokens,无顶层 cache 字段——官方缓存文档口径)
fn message_start_zhipu_nested(input_tokens: u32, cached_tokens: u32) -> String {
format!(
r#"{{"type":"message_start","message":{{"usage":{{"input_tokens":{},"prompt_tokens_details":{{"cached_tokens":{}}},"output_tokens":0}}}}}}"#,
input_tokens, cached_tokens
)
}
/// 智谱嵌套形态回退:顶层 cache 字段缺席时从 prompt_tokens_details.cached_tokens
/// 取命中量,prompt=input+cached(隐式缓存无写入分项,miss=0)
#[test]
fn anthropic_zhipu_nested_cache_tokens_fallback() {
let mut acc: Option<TokenUsage> = None;
let c = apply_anthropic_event(&message_start_zhipu_nested(500, 4000), &mut acc);
let u = c.usage.expect("usage 应随 message_start 带出");
assert_eq!(u.prompt_cache_hit_tokens, 4000);
assert_eq!(u.prompt_cache_miss_tokens, 0);
// 口径归一:prompt = input + cached
assert_eq!(u.prompt_tokens, 4500);
}
/// 顶层优先:官方 Anthropic 端点回顶层字段时嵌套值(即便异常并存)不覆盖顶层
#[test]
fn anthropic_top_level_cache_read_wins_over_nested() {
let mut acc: Option<TokenUsage> = None;
let frame = r#"{"type":"message_start","message":{"usage":{"input_tokens":100,"cache_read_input_tokens":200,"prompt_tokens_details":{"cached_tokens":999},"output_tokens":0}}}"#;
let c = apply_anthropic_event(frame, &mut acc);
let u = c.usage.expect("usage 应随 message_start 带出");
assert_eq!(u.prompt_cache_hit_tokens, 200);
assert_eq!(u.prompt_tokens, 300);
}
/// 辅助:构造 message_delta 事件(output_tokens 为累计值)
fn message_delta(output_tokens: u32) -> String {
format!(
@@ -1135,6 +1185,48 @@ mod tests {
assert!(c.usage.is_none(), "无 usage 时 message_stop usage 为 None");
}
/// 三口径映射锁定(G1,P0-A):anthropic input_tokens 不含缓存两段,落库 prompt_tokens
/// 对齐 openai/deepseek 全量输入语义(hit/miss ⊆ prompt)→ prompt = input + read +
/// creation;message_delta 的 total 重算基于累加器 prompt(已含缓存段)自动继承新口径。
#[test]
fn anthropic_message_start_cache_three_caliber_prompt_sums_cache_segments() {
let mut acc: Option<TokenUsage> = None;
let c = apply_anthropic_event(&message_start_with_cache(100, 64, 36), &mut acc);
let u0 = c.usage.expect("message_start 应带出初始 usage 快照");
assert_eq!(u0.prompt_tokens, 200, "prompt = input(100)+read(64)+creation(36)");
assert_eq!(u0.prompt_cache_hit_tokens, 64, "cache_read → hit 原样映射");
assert_eq!(u0.prompt_cache_miss_tokens, 36, "cache_creation → miss 原样映射");
assert_eq!(u0.total_tokens, 200, "初始 total 同口径");
// message_delta 累计 output 后 total 重算:继承含缓存段 prompt 基数(200+30)
let _ = apply_anthropic_event(&message_delta(30), &mut acc);
let a = acc.as_ref().unwrap();
assert_eq!(a.prompt_tokens, 200, "delta 不改 prompt");
assert_eq!(a.total_tokens, 230, "total = prompt(含缓存两段)+output");
}
/// 缺失容错(G2,P0-A):端点 200 但 usage 键整体缺失/usage 对象缺计数字段时,
/// 不再整帧反序列化失败硬炸(三路走查 #3「有效文本回复整体丢弃」),全零兜底
/// 下行走既有估算轨(round_usage_effective prompt==0 → 估算打标),对齐 OpenAI 臂。
#[test]
fn anthropic_response_missing_or_partial_usage_deserializes_not_fatal() {
// usage 键整体缺失
let resp: AnthropicResponse = serde_json::from_str(
r#"{"id":"msg_1","model":"claude-x","content":[{"type":"text","text":"hi"}],"stop_reason":"end_turn"}"#,
)
.expect("usage 键缺失不应炸帧");
assert_eq!(resp.usage.input_tokens, 0, "缺 usage → 全零兜底走估算轨");
assert_eq!(resp.usage.output_tokens, 0);
// usage 对象存在但缺计数字段(部分网关只回 cache 分项)
let resp2: AnthropicResponse = serde_json::from_str(
r#"{"id":"msg_2","model":"claude-x","content":[],"usage":{"cache_read_input_tokens":7}}"#,
)
.expect("usage 缺 input/output 字段不应炸帧");
assert_eq!(resp2.usage.input_tokens, 0);
assert_eq!(resp2.usage.cache_read_input_tokens, 7, "已有分项不丢");
}
/// content_block_start (tool_use) 带 id+name
#[test]
fn anthropic_content_block_start_tool_use() {
+54 -3
View File
@@ -69,6 +69,10 @@ pub(crate) struct AnthropicResponse {
// Anthropic 响应反序列化字段,保留以备调试/未来消费(如日志记录调用终止原因),标注意图消除 dead_code warning
#[allow(dead_code)]
stop_reason: Option<String>,
/// usage 整段 default(G2):端点不回 usage 键时不再 missing-field 炸帧,全零兜底
/// 下行走既有估算轨(round_usage_effective)。字段级容错在 AnthropicUsage 侧,
/// 此处兜「usage 键整体缺失」——两层合起来才覆盖「端点不回 usage」全形态。
#[serde(default)]
pub usage: AnthropicUsage,
}
@@ -88,9 +92,15 @@ pub(crate) struct AnthropicContentBlock {
pub input: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Default, Deserialize)]
pub(crate) struct AnthropicUsage {
/// 计数字段全部 default 容错(G2,P0-A 口径统一):部分网关 200 但 usage 缺失/缺字段,
/// 必填会让整帧反序列化失败 → 有效回复整体丢弃(三路走查 #3/#5)。缺省 0 后全零
/// TokenUsage 下行走既有估算兜底轨(round_usage_effective,prompt==0 → 估算打标),
/// 与 OpenAI 臂的 serde default 容错(openai_helpers.rs:139-157)对齐。
#[serde(default)]
pub input_tokens: u32,
#[serde(default)]
pub output_tokens: u32,
/// Anthropic prompt caching 扩展:cache 写入 token(本次写入缓存,计费如输入但稍便宜)。
/// 映射到 TokenUsage.prompt_cache_miss_tokens(全价输入语义)。
@@ -102,6 +112,32 @@ pub(crate) struct AnthropicUsage {
/// 非 cache 场景无此字段 → serde default 0。
#[serde(default)]
pub cache_read_input_tokens: u32,
/// 智谱 anthropic 兼容端点的隐式缓存(自动生效,命中半价)不回顶层 cache 字段,
/// 命中量走 OpenAI 风格嵌套 prompt_tokens_details.cached_tokens(智谱官方缓存文档
/// 口径)。serde 容错读入,消费侧经 [`Self::cache_read`] 回退取用——缺此层时
/// GLM 链路账本/命中率恒 0(实测 54 条观测全零的根因)。
#[serde(default)]
pub prompt_tokens_details: Option<PromptTokensDetails>,
}
/// OpenAI 风格嵌套的缓存命中分项(智谱 anthropic 兼容端点口径)。
#[derive(Debug, Default, Deserialize)]
pub(crate) struct PromptTokensDetails {
#[serde(default)]
pub cached_tokens: u32,
}
impl AnthropicUsage {
/// 缓存命中读取量:顶层 cache_read_input_tokens 非零优先(官方 Anthropic 端点),
/// 智谱嵌套形态(顶层缺省 0)回退;两处都不存在时 0。
pub fn cache_read(&self) -> u32 {
if self.cache_read_input_tokens > 0 {
return self.cache_read_input_tokens;
}
self.prompt_tokens_details
.as_ref()
.map_or(0, |d| d.cached_tokens)
}
}
// ============================================================
@@ -155,15 +191,30 @@ pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUs
let cache_read = u_obj
.and_then(|u| u.get("cache_read_input_tokens"))
.and_then(|t| t.as_u64())
// 智谱 anthropic 兼容端点隐式缓存走嵌套 prompt_tokens_details.cached_tokens,
// 顶层字段缺席时回退读取(与非流式 AnthropicUsage::cache_read 同表)。
.or_else(|| {
u_obj
.and_then(|u| u.get("prompt_tokens_details"))
.and_then(|d| d.get("cached_tokens"))
.and_then(|t| t.as_u64())
})
.unwrap_or(0) as u32;
let cache_creation = u_obj
.and_then(|u| u.get("cache_creation_input_tokens"))
.and_then(|t| t.as_u64())
.unwrap_or(0) as u32;
// 口径归一(G1,P0-A/u-claw 三口径): anthropic input_tokens 只含未缓存输入,
// 不含 cache_read/cache_creation 两段;落库 prompt_tokens 对齐 openai/deepseek
// 全量输入语义(prompt=全量输入,hit/miss 为其子集),故 prompt = input + read
// + creation。total 同口径(prompt + 当前 output)。
let prompt = (inp as u32)
.saturating_add(cache_read)
.saturating_add(cache_creation);
*usage_accum = Some(TokenUsage {
prompt_tokens: inp as u32,
prompt_tokens: prompt,
completion_tokens: 0,
total_tokens: inp as u32,
total_tokens: prompt,
prompt_cache_hit_tokens: cache_read,
prompt_cache_miss_tokens: cache_creation,
reasoning_tokens: 0,