端侧 4B 模型不可靠?先检查你的 Agent 架构
面向读者:正在做 RAG、Agent、私有化部署或端侧 AI 应用的工程师。
示例系统:完全离线的质量体系助手,生成模型使用 Qwen3.5-4B,嵌入模型使用 bge-m3,存储使用 SQLite,回答要求可溯源。
结论先说:4B 模型不能像云端大模型那样承担"规划、检索、判断、表达、自检"的全部职责。更稳的做法是把架构拆成记忆预算、意图仲裁、证据计划、引用门禁和受控表达。模型可以参与,但不应该拥有最终裁决权。
一、先说问题:小模型经常不是"笨",而是被架得太高
常见 Agent 链路很直接:prompt 里写清角色,把聊天记录拼进上下文,模型推理后决定是否查 RAG、是否调用工具,最后再由模型生成答案。
这种链路对大模型勉强可用,是因为大模型有较强的长上下文稳定性、格式遵循能力和隐式纠错能力。但换到端侧 4B 后,模型要同时做五件事:
- 理解当前问题;
- 继承历史对象;
- 规划检索和工具;
- 组织自然语言回答;
- 自查引用和格式。
一旦上下文变长、问法变短、工具参数变复杂,错误就会集中暴露。项目里最常见的失败不是"完全答不出",而是四类更危险的问题:
| 失败类型 | 表现 | 风险 |
|---|---|---|
| 历史继承错对象 | 上一轮问 A,这一轮短追问被接到 B | 回答流畅,但主体错了 |
| 短追问缺主语 | "还有吗""必须改的有哪些"解析成宽泛问题 | 检索范围漂移 |
| JSON / 工具参数漂移 | 多字段、多枚举、多目标时输出不稳定 | 工具调用失败或调错入口 |
| 库外编号被误放行 | 相似度高,模型就把库外标准当成命中的标准 | 高置信地编造引用 |
第三类尤其值得展开。早期评测里出现过 GJB 450A-2004 的检索相似度约 0.8015,但这个标准号并不在库内。这个问题说明:语义相似度只能衡量文本接近程度,不能证明编号真实存在。如果门禁只有相似度阈值,系统就会把"很像"当成"命中"。
所以,只往 prompt 里加"不要编造""仔细判断""严格输出 JSON"是不够的。小模型的可靠性要靠架构让它难错。
二、架构改法:把决定权从模型手里拿回来
这个项目的主链路可以压缩成下面这样:
text
用户输入
-> Context Core:有界记忆装配
-> Rule Intent:规则优先解析
-> LLM Intent Parser:受限语义提案
-> Intent Arbiter:确定性仲裁
-> Evidence Planner:生成证据计划
-> Retrieval / Tools:受控召回与工具执行
-> Quality Gate Precheck:生成前拦截
-> 4B 受控表达
-> Quality Gate Postcheck:引用后校验
-> 正常回答 / 澄清 / 拒答 / 摘录降级
核心分工是:
- 规则管边界:域外、系统指令、明确对象、材料上下文优先由确定性逻辑处理。
- 状态管继承:短追问继承的是结构化意图和目标 ID,不是自由复述聊天记录。
- 计划管检索:先确定查什么、查多少、允许用什么工具,再执行检索。
- 门禁管引用:编号、条款、来源必须在允许集合内。
- 模型管表达:4B 只在受控输入上组织语言,或者输出必须可校验的结构化提案。
这不是不给模型用语义能力,而是把语义能力放在护栏中间。
三、Context Core:历史只补信息,不重写当前意图
长会话最大的问题不是"记得少",而是"什么都记得"。如果把几十轮原文全部塞给 4B,当前问题很容易被旧主题带偏。
项目里没有把记忆做成一个聊天数组,而是拆成固定事实、滚动摘要、最近完整轮次和检索增强话题。关键预算直接写成常量:
rust
// gjb-agent/src-tauri/src/context_core.rs
pub const READ_PAIRS: usize = 12;
pub const RECENT_PAIRS: usize = 4;
pub const RETRIEVAL_CONTEXT_PAIRS: usize = 3;
pub const SUMMARY_MAX_CHARS: usize = 1_500;
pub const PROMPT_MAX_CHARS: usize = 10_000;
装配时也保持明确优先级:更早轮次进入摘要,最近 4 轮保留原文,固定事实优先于摘要,最后统一受 prompt 上限约束。
rust
let split = turns.len().saturating_sub(RECENT_PAIRS);
let (older, recent_turns) = turns.split_at(split);
render_prompt_block(
&summary,
&recent_turns,
&pinned_facts,
PROMPT_MAX_CHARS,
);
这里的重点是:历史上下文只允许补充缺失对象和任务背景,不能反过来改写当前意图。比如用户先问"技术归零是什么",再问"它和管理归零有什么区别",系统可以把"技术归零"作为补充对象;但如果当前问题已经显式切换到新对象,旧对象不能继续争夺解释权。
检索 query 也同样有界:
rust
pub fn build_retrieval_query(
question: &str,
turns: &[ConversationTurn],
) -> String {
let mut parts = vec![question.trim().to_string()];
let recent_questions: Vec<String> = turns
.iter()
.rev()
.take(RETRIEVAL_CONTEXT_PAIRS)
.map(|turn| excerpt(&turn.question, 150))
.collect();
if !recent_questions.is_empty() {
parts.push(format!("相关话题:{}", recent_questions.join(";")));
}
if let Some(last) = turns.last() {
let conclusion = excerpt(&last.answer, PRIOR_CONCLUSION_EXCERPT);
if !conclusion.is_empty() {
parts.push(format!("上一轮结论:{conclusion}"));
}
}
dedupe_query_parts(parts)
}
这样"还有吗"这类短问不会把整个历史都拖进检索,只会补充最近有限的话题和上一轮结论。
四、意图层:模型输出是提案,不是最终决定
规则意图快、可解释、稳定,但语义泛化有限。LLM 意图解析能补语义,但 4B 输出不能直接相信。项目里的做法是让两者同时存在,再由确定性仲裁器裁决。
LLM Intent Parser 的预算写得很紧:
rust
// gjb-agent/src-tauri/src/intent_parser.rs
pub const LLM_INTENT_MAX_TOKENS: u32 = 128;
pub const LLM_INTENT_TIMEOUT_MS: u64 = 1_800;
pub const LLM_INTENT_TOTAL_TIMEOUT_MS: u64 = 2_000;
超时、schema 非法、目标非法都会失败。只有可重试的 schema / object 错误才允许修复,而且第一次调用如果已经超过 200ms,就不再重试,避免短追问被拖成两次完整生成。
rust
let started = Instant::now();
let first = self.invoke(user_prompt, budget, 1, started);
match first.result {
Ok(output) => LlmIntentOutcome::succeeded(output, attempts),
Err(code) if !code.retryable() => {
LlmIntentOutcome::failed(code, attempts, false)
}
Err(_) => {
if started.elapsed().as_millis() as u64 > 200 {
LlmIntentOutcome::failed(
LlmIntentFailureCode::RetryBudgetExceeded,
attempts,
false,
)
} else {
self.retry_after_invalid(user_prompt, budget, started)
}
}
}
模型返回的内容还要过三道校验:JSON schema、封闭枚举、ID 白名单。
rust
let trimmed = raw.trim();
if !trimmed.starts_with('{') || !trimmed.ends_with('}') {
return Err(LlmIntentFailureCode::InvalidSchema);
}
let value: Value = serde_json::from_str(trimmed)
.map_err(|_| LlmIntentFailureCode::InvalidSchema)?;
let mut output: LlmIntentOutput =
serde_json::from_value(value)
.map_err(|_| LlmIntentFailureCode::InvalidSchema)?;
whitelist.validate(&mut output)?;
Ok(output)
白名单校验不是简单看类型,而是检查目标 ID、主题 ID、活跃引用 ID 是否都在当前领域上下文允许范围内:
rust
fn validate(&self, output: &mut LlmIntentOutput) -> Result<(), LlmIntentFailureCode> {
validate_closed_set(&output.target_ids, &self.target_ids)?;
validate_closed_set(&output.topic_ids, &self.topic_ids)?;
validate_closed_set(&output.referenced_intent_ids, &self.active_intent_ids)?;
validate_collection_size(&output.secondary_intents, 2)?;
validate_collection_size(&output.target_ids, 5)?;
validate_collection_size(&output.topic_ids, 3)?;
if !output.confidence.is_finite() || !(0.0..=1.0).contains(&output.confidence) {
return Err(LlmIntentFailureCode::InvalidSchema);
}
if output.needs_clarification && output.ambiguity_code.is_none() {
return Err(LlmIntentFailureCode::InvalidSchema);
}
output.topic_ids = self.project_topic_ids(&output.target_ids);
Ok(())
}
确定性仲裁器再做最后裁决。域外硬边界不交给模型:
rust
// gjb-agent/src-tauri/src/intent_arbiter.rs
if input.rule_state.domain_status == DomainStatus::OutOfDomain
|| input.snapshot.domain_guard_hint() == "reject_domain"
|| (input.rule_action == IntentAction::RejectDomain
&& input.proposal.is_some_and(|proposal| {
proposal.primary_intent == UserIntent::Unknown
}))
{
return domain_guard(input);
}
有效提案也不会直接采纳,而是先比较规则结果和模型结果是否一致,再根据显式目标、活跃引用、冲突惩罚、规则兜底加分等确定性策略裁决:
rust
let agreement = proposals_agree(input.rule_state, proposal);
let mut confidence = self.base_confidence(input, proposal, agreement);
if context_switch_has_rule_task(input, proposal) {
return rule_secondary_task_decision(input, confidence);
}
if pure_continuation_rule_beats_system_misread(input, proposal) {
return rule_decision(
input,
ArbitrationReason::RuleFallback,
confidence.max(self.policy.execute_threshold),
);
}
这一层的价值在于:LLM 单路不准,不代表不能接入主链路;只要它的输出是可拒绝、可仲裁、可降级的提案,就能参与提高语义覆盖。
五、Evidence Planner:先开检索单,再查库
很多 Agent 的检索是"模型想查什么就查什么",这在端侧 4B 上很危险。项目里检索前必须先生成 EvidencePlan,里面明确证据类型、检索 query、直接来源、输出契约和允许工具。
rust
// gjb-agent/src-tauri/src/evidence_planner.rs
pub const MAX_RETRIEVAL_QUERIES: usize = 3;
pub const MAX_DIRECT_SOURCES: usize = 8;
pub struct EvidencePlan {
pub evidence_ids: Vec<EvidenceKind>,
pub retrieval_queries: Vec<String>,
pub required_direct_sources: Vec<String>,
pub output_contract: String,
pub allowed_tools: Vec<String>,
}
域外问题不开证据,直接返回 no_evidence;需要明确对象但对象缺失时,返回 clarification,不让模型猜检索词。
rust
if state.domain_status != DomainStatus::InDomain {
return Ok(EvidencePlan {
evidence_ids: Vec::new(),
retrieval_queries: Vec::new(),
required_direct_sources: Vec::new(),
output_contract: "no_evidence".into(),
allowed_tools: Vec::new(),
});
}
if rule.requires_targets
&& state.target_ids.is_empty()
&& !current_material_scope
{
return Ok(EvidencePlan {
evidence_ids: Vec::new(),
retrieval_queries: Vec::new(),
required_direct_sources: Vec::new(),
output_contract: "clarification".into(),
allowed_tools: Vec::new(),
});
}
所有计划也会做数量收敛:
rust
direct_sources.truncate(MAX_DIRECT_SOURCES);
语义召回当然有用,但它必须被夹在证据计划中间:召回前知道范围和上限,召回后还要能校验来源。
六、Quality Gate:生成前后都要有门禁
生成前门禁不是走过场,而是检查任务路由、工具白名单、超时预算和证据状态。证据缺失时不进入 LLM。
rust
// gjb-agent/src-tauri/src/quality_gate.rs
pub fn precheck(input: PrecheckInput<'_>) -> PrecheckOutcome {
if input.elapsed_ms >= input.timeout_ms {
return rejected("run_timeout");
}
if input.route_id.is_none() {
return rejected("route_unmatched");
}
if let Some(tool) = input.required_tools.iter().find(|tool| {
!input.allowed_tools.iter().any(|allowed| allowed == **tool)
}) {
return rejected_tool(tool);
}
if !input.has_public_evidence
&& !input.has_material
&& !input.has_template
{
return rejected("evidence_missing");
}
PrecheckOutcome {
passed: true,
code: None,
}
}
生成后门禁继续校验标准号和条款号。允许集合不是模型自己声明的,而是由本轮命中文档、片段和结构化上下文构造出来的。
rust
let mut allowed_docs =
allowed_from_docs(hits.iter().map(|hit| hit.doc.clone()));
allowed_docs.extend(
hits.iter()
.flat_map(|hit| profile.extract_standard_codes(&hit.snippet))
.map(|token| normalized_code(&token)),
);
let bad_standards = unknown_tokens(
profile.extract_standard_codes(&answer),
&allowed_docs,
);
let (cleaned, removed_standards, _) =
strip_unknown_token_sentences(&answer, &bad_standards);
if removed_standards > 0 {
violations.push(repaired(
"citation_standard_removed",
"答案含非本轮引用的标准号",
false,
));
answer = cleaned;
}
也就是说,模型可以写出一段流畅回答,但如果其中引用了本轮证据之外的标准号,包含这个引用的句子会被剥离。这个动作可能让回答变得保守,但比"高置信编造"更可接受。
七、验证结果:单路不稳,主链路可控
架构不能只讲故事。项目用封闭评测集、真实链路压测和功能回归来验证。
1. 语义意图评测
一组 60 例语义评测的对比:
| 指标 | Rule 单路 | LLM 单路 | Arbiter 主链路 |
|---|---|---|---|
| 意图准确率 | 60.00% | 76.67% | 100.00% |
| 上下文继承准确率 | 54.17% | 83.33% | 100.00% |
| 对象准确率 | 70.27% | 89.19% | 100.00% |
| 域外误放行 | - | 0 | 0 |
| 误澄清率 | - | 1.67% | 0 |
| LLM P95 延迟 | - | 1600ms | 1600ms |
这组数据容易被误读成"4B 达到 100%"。更准确的解释是:Rule 和 LLM 单路都没有达到稳定接主链路的水平,但经过确定性仲裁、域外守卫和白名单校验后,主链路在这组封闭题集里表现可控。
2. 引用压力集
100 题连续压力集首轮为 98/100,暴露的是指代链和证据兜底长度问题。修复指代词表、当前任务继承和硬上限后,完整重跑达到 100/100。
这里的重点不是"永远满分",而是失败能被定位到具体机制,并且能进入回归。
3. 多功能真实链路回归
另一组覆盖标准问答、模板中心、研制过程评审、评审模拟和资格审查的评测:
| 项 | 结果 |
|---|---|
| 真实链路执行 | 750 次 |
| 唯一题目 | 450 道 |
| 三轮机器判定 | 均通过 |
| 引导题 | 86/86 命中目标功能 |
| 落库展示一致 | 750/750 |
这组测试里只有 138 题进入生成回答,大量问题由结构化引导、模板目录、规则知识或审查结果直接回答。这也说明一个端侧 Agent 的稳定性,不只来自"模型答得好",还来自"很多问题根本不需要把解释权交给模型"。
当然要保留边界:这些是封闭域、固定题集、带校验和仲裁的机器判定结果,不能直接推广为开放域能力,也不能替代业务盲评。
八、可复用的工程经验
1. 不要让 4B 同时做规划、检索、判定和表达
大模型 Agent 可以把多角色压在一个推理过程里,4B 不适合。更稳的结构是每个模块只负责一件可验证的事。
2. 约束要写成常量和契约,不要写成提示词态度
"最近 4 轮保留原文""query 最多 3 条""来源最多 8 个""单次解析 1800ms"这类规则,应该出现在代码、配置和 schema 里,而不是只出现在系统提示词里。
3. 模型输出必须可拒绝
JSON schema、封闭枚举、目标白名单、置信度范围、重试次数、总延迟都要校验。模型解析失败时,系统应该回退规则路径,而不是把非法输出继续往下传。
4. 检索不是自由行动,而是受控任务
先由意图和状态生成 Evidence Plan,再执行检索。query 数量、来源数量、允许工具、输出契约都应该提前声明。
5. 相似度阈值不是引用门禁
相似度只能帮助排序,不能证明标准号、条款号真实存在。引用校验必须基于本轮证据、库内编号和结构化上下文构造允许集合。
6. 拒答和澄清是系统能力
no_evidence、clarification、evidence_missing、citation_standard_removed 不是失败文案,而是把不确定挡在系统边界外的机制。端侧垂直 Agent 更需要这种保守性。
7. 判断一个端侧 Agent,先问四个问题
- 意图谁仲裁?
- 证据谁选择?
- 引用谁校验?
- 超时怎么降级?
这四个问题答不清楚,换更大的模型也只是把错误往后推。
落地检查清单
- 列出模型当前能决定的所有事项,把域外、对象继承、工具选择和最终引用裁决移到确定性模块。
- 为上下文、检索、LLM 解析和总链路定义显式预算,用常量或配置锁住,不允许调用方临时放宽。
- 给每个模型输出定义 schema、封闭枚举、ID 白名单、超时和失败回退路径。
- 在检索前生成 Evidence Plan,明确 query 数量、来源数量、证据类型、允许工具和输出契约。
- 建立四类回归:Rule 单路、LLM 单路、Arbiter 主链路、真实链路压测;指标至少覆盖意图、继承、域外误放行、引用违规和延迟。
结语
端侧 4B 的正确用法,不是把它当成缩小版专家,而是给它一张受控工单:输入有界、工具白名单化、证据可计划、输出可校验、失败可降级。
这个项目的实践可以压缩成一句话:
规则管边界,状态管继承,计划管检索,门禁管引用,模型管表达。
当架构先把错误路径拦住,4B 模型反而能在一个很窄但稳定的位置上发挥价值。
说明:文中数据来自示例系统内部评测记录,评测集版本、硬件、并发和阈值变化后,结果不能直接横向比较。