面试知识库

22 · Agent 软/硬约束分层清单(Prompt 软约束 vs 代码硬约束 · 防御纵深工程实践)#

目的:把 DocMind 对 Agent 的全部约束按 「prompt 软约束」与「代码硬约束」 两层完整盘点,逐条标注代码位置、约束机制,并对标主流 LLM Agent 安全工程范式(OWASP LLM Top 10 2025、Anthropic《Building Effective Agents》、NeMo Guardrails / Guardrails AI、allowlist-over-denylist / least-privilege),论证「这是不是主流工程实践」。

配套:本篇是 02-Agentic-RAG核心设计(agentic 循环本体)、05-工程化实践(LLM 工程实践 / 安全体系)、21-权限系统对齐报告(权限/MCP 安全边界)的横切总结——前三篇分散讲「某个组件怎么实现」,本篇回答一个独立问题:「对 Agent 的每一条约束,到底是求模型听话,还是代码硬拦?为什么这样分?」

调研时间:2026-06。代码行号对应当前 main 分支,来源见文末。


0 · 执行摘要#

DocMind 对 Agent 的约束是一套 「软约束引导意图 + 硬约束兜底防越界」 的双层结构,一句话原则:

凡是 prompt 能被模型「不听话」绕过、且后果涉及权限 / 安全 / 成本的约束,绝不只靠 prompt——必须有确定性代码兜底。检索策略、作答风格、判断倾向这类「错了也只是质量打折」的约束,才交给 prompt 软引导。

这套分层是教科书级的主流工程实践,并非自创:它就是 防御纵深(defense-in-depth) 在 LLM Agent 上的标准落法,能逐条对上 OWASP LLM06「Excessive Agency」的缓解清单、Anthropic「在 agent 外层放确定性 guardrails」的主张、以及 allowlist / least-privilege 安全默认。

维度软约束(Prompt)硬约束(代码)
载体SYS_PROMPT、*.txt 模板、内联 promptJava 正则 / 白名单 / 上下文覆盖 / 循环上限
能否被模型绕过能(模型可不听)不能(物理上做不到)
失败后果质量打折(少引用、啰嗦、误判意图)越权 / 烧钱 / 安全事故
典型「searchDocs 优先」「必须 [n] 引用」工具白名单、kbIds 覆盖、轮数上限、紧急词短路
数量6 类(5 个 prompt 模板 + SYS_PROMPT)16+ 处

两条诚实差距(见 §5):①外部 MCP 调用仍可传任意 kbIds(硬控只在内部会话上下文生效);②[n] 引用是「软约束 + 硬校验但不硬阻断」。


1 · 这是主流工程实践吗?——对标四条范式#

结论:是。 这套软/硬分层不是项目特例,而是 2024–2025 年 LLM Agent 安全工程的共识做法。逐条对标:

1.1 OWASP LLM Top 10 (2025) — LLM06 Excessive Agency#

OWASP 对「过度自主」的缓解清单第一条就是 「最小权限工具(minimize extensions/permissions)+ 在工具/代码侧做 deterministic 校验,而非依赖 LLM 自我克制」。DocMind 的命中:

  • 只读工具白名单(只放 3 个 read 工具,写工具不注册)→ 对应「minimize tool functionality」。
  • kbIds 强制覆盖、userId 注入 → 对应「execute in user’s context, not LLM-supplied identity」。
  • 轮数上限 + 空轮护栏 → 对应「limit the rate/scope of autonomous actions」。

反范式(被本项目规避):把「不要调写工具」「不要越权」只写进 prompt 求模型遵守——OWASP 明确指出 prompt 约束不能算作 agency 控制,因为它可被 prompt injection 绕过。

1.2 Anthropic《Building Effective Agents》(2024)#

Anthropic 主张「在 agent 外层放确定性 guardrails、收窄工具面、让 agent loop 可控可停」,并强调 agentic 系统要「为模型设计清晰的工具与停止条件」。DocMind 的命中:

  • internalToolExecutionEnabled(false) 自己手动控环(不用框架的无上限内部循环)→ 「loop 可控」。
  • 空轮 early-stop 护栏 → 「停止条件由代码兜底,而非全靠模型自觉」。
  • SYS_PROMPT 明确工具职责边界 + buildSeed 引导「搜空即止」→ 「为模型设计清晰工具语义」(软层)。

1.3 NeMo Guardrails / Guardrails AI — programmatic rails#

主流 guardrails 框架的核心理念是 「把安全 / 范围 / 格式约束做成围着 LLM 的代码栏杆(programmatic rails),而非 prompt 里求它」。DocMind 未引入框架,但自研了等价物

  • 输入侧 rail:SafetyGuard.isEmergency() 关键词短路(紧急情形不进 LLM)、MetaIntentDetector 正则 scope 路由。
  • 输出侧 rail:CitationParser 事后解析 [n]、算覆盖率、标 invalidRefsungrounded 观测信号。

1.4 allowlist-over-denylist + least-privilege#

安全工程默认正确做法是「白名单 + 最小权限」。DocMind 用 白名单(只把 4 个读工具 + 1 个沙箱计算工具 executeCode 放进 toolCallbacks,硬排除 store_memory / kb_meta)而非黑名单,代码注释甚至点明原理:

「必须用过滤而非 toolNames:Spring AI 的 toolNames 是追加语义(与 toolCallbacks 取并集),无法排除 store_memory;只有不把它放进 toolCallbacks 才能真正屏蔽。」——AgenticSearchOrchestrator.java:167-178

一句话定性:DocMind 这套约束分层对得上以上四条主流范式的每一条,属于「正确但朴素」的工程实践——朴素在于自研而非引入 NeMo Guardrails 等成熟框架,正确在于分层原则与边界判断与主流完全一致。


2 · 代码硬约束清单(deterministic / 模型无法绕过)#

#约束代码位置机制对应主流范式
H1只读工具白名单AgenticSearchOrchestrator.java:65 ALLOWED_TOOLS:173 whitelistedToolCallbacks(){searchDocs,webSearch,recall_memory}store_memory/kb_meta 不放进 toolCallbacks(allowlist 而非 denylist)OWASP LLM06 / least-privilege
H2kbIds 权限覆盖DocSearchTool.java:53-59AgentToolContext.isActive() 时用户会话 kbIds 强制覆盖 LLM 传入值,防幻觉越权访问其它库LLM06 / 检索层权限(见 [21])
H3userId 注入MemoryTool.java:200-202 resolveUserId()完全忽略外部/LLM 传入 userId,只取 auth 上下文;无上下文则记忆操作降级 no-opLLM06 / 身份不可由 LLM 指定
H4循环轮数上限AgenticSearchOrchestrator.java:103:135maxIters(默认 4)+ internalToolExecutionEnabled(false) 手动控环;故意不把上限告诉模型(告知会锚定模型跑满预算)Anthropic loop 可控 / 成本上限
H5空轮护栏 early-stopAgenticSearchOrchestrator.java:128-155本轮零新增 chunk 且已有积累 → 强制 break。注释:「仅靠 SYS_PROMPT 约束不够(模型常跑满预算),故代码侧硬兜底」guardrails / 停止条件
H6温度归零AgenticSearchOrchestrator.java:110temperature(0.0),检索决策确定性化可复现
H7写记忆走直连 JavaDocMindAgent.java:737-750 stageMemoryWrite()内部记忆写入只经直接 Java 调用,不暴露给 LLM 工具循环(与 H1 互补:双重防 LLM 误写)least-privilege
H8紧急词前置短路DocMindAgent.java:131SafetyGuard.java:32-80isEmergency() 关键词/正则匹配,在缓存与任何 LLM 之前短路;强情境词单独命中、应急主题词 ∩ 紧迫词共现才触发输入侧 rail(NeMo input rail)
H9Scope Tier-0 规则路由MetaIntentDetector.java 整类纯正则识别闲聊/身份声明/元对话/KB_META,precision>recall,命中即定零 LLM 成本规则前置 / 成本优化
H10路径决策 routePathDocMindAgent.java:1117-1144基于分类 flag + kbIds 数量 + 正则意图,确定性三选一(SELECTED_DOC / RULE_PLANNER / AGENTIC),不让 LLM 决定走哪条路Adaptive RAG 路由
H11分类器确定性后处理QueryUnderstandingService.java:187-211多焦点强信号正则 / ≥2 问号命中 → 强制把 complexity 抬到 MEDIUM,覆盖 LLM 自报值(防漏判多焦点漏走 agentic)LLM 输出后校正
H12SELECTED_DOC 直读触发DocMindAgent.java:1146-1150 matchesDirectReadIntentDOCUMENT_REFERENCE_PATTERN ∩ SUMMARY_INTENT_PATTERN 正则 + kbIds 数量门控,确定性判定是否绕过检索直读规则路由
H13收尾管道 finalizeAgenticSearchOrchestrator.java:182-250去重→Cross-Encoder rerank→MMR→压缩→CRAG 评分,全 Java,模型无权干预证据筛选确定性后处理
H14兜底 / 低置信判定AgenticSearchOrchestrator.java:248DocMindAgent.java:900-907needsFallback = compressed.isEmpty() 等纯代码条件决定走哪套模板,非模型自判输出 rail
H15置信度 & 引用校验DocMindAgent.java:994-1011CitationParser 纯 Java 解析 [n]、算覆盖率、标 invalidRefs、判 ungrounded;置信度 = clamp(coverage) × rerank-top1输出侧 rail(事后校验)
H16记忆提取触发(LLM 即 Gate)DocMindAgent.java stageAsyncMemoryExtract无规则预筛——每轮回答后都异步触发提取,是否有可记内容由提取 LLM 返回 NOOP 自裁(recall 优先,对标 Mem0/LangMem/Zep);唯一硬前置是 userId 为空跳过。早期两层规则 Gate 已删(伤 recall),省成本改由异步 + 廉价模型(memory.extract_model,默认 qwen-flash)消化成本控制 / recall 优先

3 · Prompt 软约束清单(靠 LLM「听话」)#

#约束位置内容要点
S1检索规划器系统提示AgenticSearchOrchestrator.java:67-82 SYS_PROMPT不负责作答;searchDocs 优先 / webSearch 仅当知识库不足 / recall 仅当需个性化;多焦点分别检索;多跳先查中间事实;搜空即止、不近义改写重搜同一源;不调写工具;不传 kbIds;用尽量少轮数
S2检索 seed 引导AgenticSearchOrchestrator.java:319-330 buildSeed「尽量少的工具调用、搜空即止」;故意不告知轮数预算(防锚定跑满)——软引导,硬上限在 H4
S3意图/复杂度/scope 判定原则query_understanding.txt身份声明归 CHITCHAT;拿不准归 KNOWLEDGE_QUERY;timeAware 判定示例;多焦点应判 MEDIUM/COMPLEX;memoryWriteHints 只填明确声明
S4作答纪律(主模板)knowledge_qa.txt严格基于来源不得编造;[n] 引用「强制」;知识库 vs 网络双源可信度限定语;≤600 字精炼;冲突指出各自来源;不重复输出来源清单
S5低置信防陈旧幻觉knowledge_qa_low_confidence.txt开头加 ⚠️ 相关性偏低提示;严禁否定来源中以完整名称出现的实体(防退回陈旧基座记忆产生时效性幻觉);缺方明确告知
S6历史短路 / 闲聊作答纪律DocMindAgent.java:503-535 内联 prompt仅基于历史不编造;身份声明仅表示已记录、不展开职业介绍、不捏造年限技能
S7安全 / 记忆抽取 LLM 判断safety_check.txt、memory_extract.txtLLM 侧的风险判别与记忆抽取,结果再经代码 Gate / 阈值过滤

4 · 软硬双层防御(hybrid)——本项目最值得讲的设计#

最能体现「主流工程素养」的不是某条硬约束,而是 同一约束两边都写:prompt 先礼(让模型理解意图、行为更自然),代码后兵(模型不听时硬拦)。凡涉及成本/权限的约束,从不只靠 prompt。

意图软层(prompt 先礼)硬层(代码后兵)为何两边都要
不写记忆S1「不要调用任何写入类工具」H1 白名单硬排除 store_memory + H7 写记忆走直连prompt 让模型不去尝试(省一次失败调用),白名单保证即使尝试也调不到
不越权访问库S1「不要在参数里传 kbIds」H2 ctx 强制覆盖prompt 减少模型传脏参,覆盖保证传了也无效
尽快停止、别重搜S1 第 4 条 + S2 buildSeedH5 空轮 early-stop 护栏prompt 引导理想行为,护栏兜「模型跑满预算」的常见现实
别烧钱跑满轮S2「尽量少的工具调用」H4 maxIters 硬上限 + 不告知预算软引导「尽量少」,硬上限封顶,且刻意不暴露预算防锚定
必须 [n] 引用S4「引用标注(强制)」H15 CitationParser 校验覆盖率 + invalidRefsprompt 求模型引用,校验把「引用质量」量化成置信度信号
多焦点走 agenticS3「多焦点应判 MEDIUM」H11 正则强制抬 complexityprompt 让分类器尽量判对,正则兜模型漏判
紧急情形特殊处理(无软层,直接硬短路)H8 SafetyGuard 关键词短路安全关键路径不给模型机会,纯硬控

设计哲学一句话:软约束负责「让模型在 99% 情况下行为正确且自然」,硬约束负责「让那 1% 不听话也不会造成不可逆后果」。两者不是冗余,是纵深——软层降低硬层被触发的频率,硬层兜住软层失效的后果。


5 · 边界与诚实清单(面试防翻车)#

这套约束不是无懈可击,主动说出边界比被追问到更显成熟:

  1. 硬控只在内部会话上下文生效。H2 / H3 的强制覆盖依赖 AgentToolContext.isActive()外部 MCP 直连(持 API-Key)时 ctx 不 active,仍可传任意 kbIds / 此前可传任意 userId。当前 MCP 已加 X-API-Key 鉴权层(不再匿名),但「kbIds 归属校验 / userId 从 auth 注入」仍是生产加固项——详见 21-权限系统对齐报告CLAUDE.md MCP 安全边界表。

  2. [n] 引用是「软约束 + 硬校验」但不是硬阻断。H15 的 CitationParser 只做事后解析与覆盖率打分,不会拦截没引用的答案;不达标只反映为低 confidenceScore / ungrounded 观测信号(DocMindAgent.java:1009),不会重写或拒绝输出(自反思已删除,生成即终态)。换言之,引用纪律的「最后一公里」仍依赖模型听话 S4。

  3. 部分软约束无硬兜底。如「≤600 字精炼」「双源可信度限定语」「冲突指出各自来源」(S4)纯靠模型,无代码校验——这些「错了只是质量打折、不涉及安全」,按 §0 原则本就不该上硬控,属于有意为之而非疏漏。

  4. prompt injection 面。H1/H2 能挡「模型自己幻觉越权」,但对「检索到的 KB/Web 内容里夹带注入指令诱导模型」目前无专门 rail(OWASP LLM01)。这是 RAG 系统的通用未尽项,非本项目特有。


6 · 面试话术 / Q&A#

Q:你怎么决定一个约束用 prompt 还是用代码? A:一条判据——「模型不听话的后果是什么」。后果是质量打折(少引用、啰嗦、意图误判),用 prompt 软引导即可;后果是越权 / 烧钱 / 安全事故,必须代码硬控。比如「searchDocs 优先」错了只是多搜一次,软约束;「不写记忆」错了会污染用户长期记忆,硬控(白名单不注册写工具)。

Q:既然有代码硬控,为什么还在 prompt 里重复写一遍? A:纵深防御,不是冗余。软层让模型 99% 情况行为正确且自然、降低硬层被触发频率(少一次失败的工具调用、少传脏参);硬层兜住那 1%。典型如轮数控制:prompt 引导「尽量少」让多数 query 自然早停,maxIters 硬上限兜「模型跑满预算」这个实测常见现象——而且我故意不把上限写进 prompt,因为告诉模型预算会把它锚定到用满。

Q:这套做法主流吗? A:是 OWASP LLM06「Excessive Agency」缓解清单的标准落法——最小权限工具 + deterministic 校验,而非依赖 LLM 自我克制。也对得上 Anthropic「在 agent 外层放确定性 guardrails」、NeMo Guardrails 的 programmatic rails、allowlist-over-denylist。我用白名单而非黑名单排除写工具,代码注释还点明 Spring AI 的 toolNames 是追加语义无法排除,只能靠不注册——这正是 allowlist 的精髓。

Q:最大的安全短板在哪? A:硬控依赖内部 AgentToolContext 激活,外部 MCP 直连这条路径绕过了 kbIds 强制覆盖。当前加了 API-Key 鉴权止血,但「检索层权限下推 + kbIds 归属校验」还是 P1 加固项。我不会假装它已经做完——这条在文档 21 里有完整的对齐方案。


来源#

  • OWASP Top 10 for LLM Applications 2025 — LLM06: Excessive Agency / LLM01: Prompt Injection(缓解清单:最小权限工具、deterministic 校验、人机/代码侧把关)。
  • Anthropic, Building Effective Agents(2024):确定性 guardrails、收窄工具面、可控可停的 agent loop。
  • NVIDIA NeMo Guardrails / Guardrails AI:programmatic input/output rails 范式。
  • 安全工程通则:allowlist-over-denylist、principle of least privilege。
  • 本项目代码:AgenticSearchOrchestrator / DocMindAgent / DocSearchTool / MemoryTool / MetaIntentDetector / QueryUnderstandingService / SafetyGuard / CitationParser + src/main/resources/prompts/*.txt(行号见正文)。
  • 配套文档:02 / 05 / 21 / CLAUDE.md(MCP 安全边界表)。