File size: 7,133 Bytes
c55968e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | //! # perch-ultrafast
//!
//! **混合流 + Ultrafast 推理框架**——把"用户感觉慢"从架构层面解决。
//!
//! 从 [栖木 Perch](https://huggingface.co/hongcoding/perch)(macOS 本地优先 AI 工作台)
//! 中提取的可复用 Rust crate。
//!
//! ## 解决的问题
//!
//! 云端大模型首 token 延迟 0.5~2s,用户盯着空屏幕等。本机小模型首 token 0.05~0.2s。
//!
//! **混合流**:两条流同时开——本机先出草稿(用户立刻有东西读),云端一到就接棒
//! (质量升级)。首字延迟 = 本机延迟,最终质量 = 云端质量。
//!
//! ## 快速开始
//!
//! ```rust,no_run
//! use std::sync::Arc;
//! use perch_ultrafast::{hybrid::{run_hybrid, HybridConfig}, types::*};
//! use tokio_util::sync::CancellationToken;
//!
//! # async fn demo(local: Arc<dyn Provider>, cloud: Arc<dyn Provider>, req: ChatRequest) {
//! let cancel = CancellationToken::new();
//! let result = run_hybrid(
//! local.clone(),
//! cloud,
//! ChatRequest { think: Some(ThinkLevel::Off), ..req.clone() }, // 本机:关思考求快
//! req, // 云端:原样
//! cancel,
//! HybridConfig::default(),
//! |chunk| match chunk {
//! StreamChunk::Delta { text } => print!("{text}"),
//! StreamChunk::Replaced { .. } => println!("\n[云端接棒]"),
//! StreamChunk::Done { .. } => println!("\n[done]"),
//! _ => {}
//! },
//! )
//! .await;
//! println!("最终 {} 字,云端接棒 = {}", result.content.chars().count(), result.used_cloud);
//! # }
//! ```
//!
//! ## 两个模块
//!
//! - [`hybrid`]:混合流 mux(本机草稿 → 云端接棒,含双向降级)
//! - [`ultrafast`]:加速规则(关思考 / 回复预算 / 简洁提示 / **模型锁红线**)
//! - [`pool`]:连接池单例(复用 TCP + TLS,启用压缩)
//!
//! ## 实测数据(栖木 Perch,MiMo 云端 + 本机 qwen3-0.6B)
//!
//! | 场景 | 首字延迟 | 说明 |
//! |---|---|---|
//! | 纯云端 | 0.88~1.39s | 用户盯着空屏幕 |
//! | 本机热身 | **0.04~0.18s** | 混合流草稿 |
//! | 混合流首字 | **< 0.2s** | 本机草稿立刻可读 |
//! | 云端接棒 | ~0.9s | 无缝替换,质量升级 |
//!
//! ## 设计红线
//!
//! - **加速绝不换模型**(不偷偷路由到小模型)
//! - **只改本次下发值**(不动用户持久化档位)
//! - **云端失败保留草稿**(不把用户的答案变成错误)
//! - **本机失败静默降级**(行为等同没用混合流,不打扰用户)
pub mod hybrid;
pub mod pool;
pub mod types;
pub mod ultrafast;
pub use hybrid::{run_hybrid, HybridConfig, HybridResult};
pub use types::{
BoxFuture, ChatMessage, ChatRequest, MessageRole, Provider, ProviderStream, StreamChunk,
ThinkLevel,
};
pub use ultrafast::{apply_ultrafast, UltrafastConfig};
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
/// 测试用 provider:按给定脚本吐 chunk(真异步,不阻塞 runtime)。
struct ScriptedProvider {
chunks: Vec<StreamChunk>,
delay_ms: u64,
}
impl Provider for ScriptedProvider {
fn chat_stream(
&self,
_request: ChatRequest,
_cancel: CancellationToken,
) -> BoxFuture<Result<ProviderStream, String>> {
let chunks = self.chunks.clone();
let delay = std::time::Duration::from_millis(self.delay_ms);
Box::pin(async move {
let (tx, rx) = tokio::sync::mpsc::channel::<StreamChunk>(16);
tokio::spawn(async move {
for chunk in chunks {
tokio::time::sleep(delay).await;
if tx.send(chunk).await.is_err() {
return;
}
}
});
let s = futures::stream::unfold(rx, |mut rx| async move {
rx.recv().await.map(|chunk| (chunk, rx))
});
Ok(Box::pin(s) as ProviderStream)
})
}
}
fn request() -> ChatRequest {
ChatRequest {
model: "m".to_string(),
messages: vec![],
think: None,
max_tokens: None,
tools: None,
}
}
#[tokio::test]
async fn cloud_takes_over_and_replaces_draft() {
let local = Arc::new(ScriptedProvider {
chunks: vec![
StreamChunk::Delta { text: "draft".into() },
StreamChunk::Done {
prompt_tokens: None,
completion_tokens: None,
eval_ms: None,
cached_tokens: None,
},
],
delay_ms: 0,
});
let cloud = Arc::new(ScriptedProvider {
chunks: vec![
StreamChunk::Delta { text: "final".into() },
StreamChunk::Done {
prompt_tokens: None,
completion_tokens: None,
eval_ms: None,
cached_tokens: None,
},
],
delay_ms: 30,
});
let mut events = Vec::new();
let result = run_hybrid(
local,
cloud,
request(),
request(),
CancellationToken::new(),
HybridConfig::default(),
|c| events.push(c),
)
.await;
assert!(result.used_cloud, "云端应该接棒");
assert_eq!(result.content, "final");
let replaced = events.iter().any(|c| matches!(c, StreamChunk::Replaced { .. }));
assert!(replaced, "接棒时应下发 Replaced 让前端清草稿");
}
#[tokio::test]
async fn cloud_failure_falls_back_to_local() {
struct FailingProvider;
impl Provider for FailingProvider {
fn chat_stream(
&self,
_request: ChatRequest,
_cancel: CancellationToken,
) -> BoxFuture<Result<ProviderStream, String>> {
Box::pin(async { Err("cloud down".to_string()) })
}
}
let local = Arc::new(ScriptedProvider {
chunks: vec![
StreamChunk::Delta { text: "local only".into() },
StreamChunk::Done {
prompt_tokens: None,
completion_tokens: None,
eval_ms: None,
cached_tokens: None,
},
],
delay_ms: 0,
});
let result = run_hybrid(
local,
Arc::new(FailingProvider),
request(),
request(),
CancellationToken::new(),
HybridConfig::default(),
|_| {},
)
.await;
assert!(!result.used_cloud);
assert_eq!(result.content, "local only", "云端挂了应保留本地草稿");
assert!(result.finished);
}
}
|