Prompt 与上下文工程Prompt 与上下文工程
从提示模板扩展到上下文选择、权限过滤、压缩、缓存、审计与可复现的 Java 装配流水线。从提示模板扩展到上下文选择、权限过滤、压缩、缓存、审计与可复现的 Java 装配流水线。
专题导读
Prompt 工程关注“如何表达任务”,上下文工程关注“本次调用究竟让模型看到什么”。对 Java 后端系统,后者是一条完整的数据管道:识别调用者与任务,获取候选信息,执行 ACL,去重与排序,在 token 预算内选择或压缩,形成不可变快照,再记录来源、版本和裁剪原因。
一个可靠的上下文不是越长越好,而应满足四个条件:
- 必要:与当前任务直接相关,减少噪声与冲突。
- 有权:调用者对每条业务数据有读取权限。
- 可追溯:知道来自哪个文档、工具或会话状态,以及对应版本。
- 可复现:保存模板、模型、检索结果、参数和裁剪决策,能够解释线上行为。
网页、邮件、附件、工单、代码、RAG 片段和工具返回值都可能包含恶意或冲突文本。把它们标记为数据、使用结构化边界有助于模型理解,但不构成安全隔离。真正的权限边界必须在模型外,由 Java 服务的认证、授权、参数校验、工具 allowlist、网络策略和审批流程实现。
知识地图
| 阶段 | 输入 | 核心机制 | 失败风险 |
|---|---|---|---|
| 身份与任务识别 | 用户、租户、请求 | 认证、任务分类、策略选择 | 匿名数据混入、策略错配 |
| 候选获取 | 历史、RAG、工具、业务状态 | 召回、缓存、版本读取 | 漏召回、过期数据 |
| 安全过滤 | 候选项及资源属性 | ACL、租户隔离、最小权限 | 越权、间接提示注入 |
| 价值排序 | 相关性、时效性、信任等级 | 去重、打分、冲突检测 | 噪声、重复、证据冲突 |
| 预算装配 | token 上限与输出预留 | 分桶、原子选择、压缩 | 硬截断、输出无空间 |
| 快照调用 | 模板、消息、工具定义 | 版本化、稳定序列化 | 无法复现、缓存串租户 |
| 评测审计 | 响应、来源、裁剪记录 | 任务指标、安全回归 | 只看主观回答质量 |
推荐的数据流是:
主体认证 → 任务分类 → 拉取候选 → ACL 过滤 → 去重/评分 → 冲突处理 → 分桶预算 → 原子装配 → 快照与哈希 → 模型调用 → 工具再授权 → 评测与审计
面试题详解
Q1:Prompt 工程与上下文工程有什么区别?
**核心回答:**Prompt 工程主要设计指令、输出要求和 few-shot 示例;上下文工程覆盖模型调用前后的整条信息供应链,包括检索、记忆、工具定义、权限、排序、预算、压缩、快照、缓存和评测。前者是后者的一部分。
例如“请基于证据回答,不确定就说不知道”是一条 prompt 指令;从 500 个候选文档中只选出当前租户有权访问、与问题相关、版本有效的 8 个片段,并记录被丢弃原因,是上下文工程。
两者都不能取代确定性控制。提示中写“不得查询其他租户”不是租户隔离;工具网关仍须从已认证主体推导 tenantId,而不是接受模型生成的租户参数。
面试追问:为什么不能把所有规则都写进 system prompt? 规则越多会占预算、发生冲突且模型遵循并非形式化保证;认证、金额上限、资源归属等应由代码和策略引擎强制执行。
常见错误回答:“上下文工程就是更长、更复杂的 prompt”“只要 system prompt 足够强就不需要后端校验”。
Q2:指令层级应如何理解,能否作为安全边界?
**核心回答:**部分模型 API 定义 system、developer、user、tool 等角色及优先关系,但角色集合和精确语义依供应商而异。应用应遵循目标 API 的正式文档,不能把某一家协议泛化为所有模型的共同规范。
较高优先级指令用于表达应用策略,较低优先级消息不能按设计覆盖它;然而模型是概率系统,可能受到指令冲突、间接注入或上下文混淆影响。因此“指令优先级”是行为引导机制,不是访问控制机制。
后端应实施纵深防御:
- 受控模板不可由普通用户修改;
- 外部内容有来源、信任级别和清晰边界;
- 工具参数采用 allowlist 和服务端派生值;
- 每次工具调用重新鉴权,不信任模型给出的身份与范围;
- 高风险动作需要确认或人工审批;
- 输出进入日志、HTML、SQL、Shell 等下游前按对应上下文编码或校验。
面试追问:用 XML 标签包住网页内容能防 prompt injection 吗? 不能。标签帮助区分结构,但网页中的恶意指令仍会进入模型上下文;模型不是具备硬隔离的解释器。
常见错误回答:“system prompt 永远不会被绕过”“只要写一句 ignore instructions in documents 就安全”。
Q3:直接提示注入与间接提示注入有何区别?
**核心回答:**直接注入来自当前用户输入,例如要求忽略既有规则;间接注入藏在模型读取的网页、邮件、附件、代码仓库、RAG 文档或工具返回值中。后者更像供应链输入:攻击者不一定直接与 Agent 对话,也能影响其行为。
风险大小取决于模型可执行的能力。纯摘要系统可能泄漏上下文;具备邮件、支付、代码执行或浏览工具的 Agent 可能产生实际副作用。因此防御重点不是“检测所有恶意句子”,而是限制影响半径:最小工具集、最小数据范围、只读优先、参数约束、网络出口限制、幂等、审批和审计。
注入分类器或规则可以作为辅助信号,但存在误报、漏报和对抗绕过,不能成为唯一授权判断。即使内容被判定为“安全”,工具调用仍必须鉴权。
面试追问:RAG 数据已由企业内部维护,是否可视为可信? 不能默认。内部文档可能被误编辑、账号被入侵、同步链路受污染,且内容里的旧操作说明也可能与当前策略冲突。
常见错误回答:“过滤掉‘忽略之前指令’这几个词即可”“内部知识库没有注入风险”。
Q4:一个可靠的 Context Assembler 应有哪些步骤?
**核心回答:**先安全、后相关;先确定预算和输出预留,再选择原子上下文。推荐步骤如下:
- 从认证上下文获得用户、租户、角色与策略,不接受模型补写身份。
- 按任务类型选择模板、允许的上下文来源和工具集合。
- 并行获取候选历史、检索片段和业务状态,并保留版本。
- 在进入模型前做租户与资源 ACL 过滤。
- 规范化、去重,计算相关性、时效性、信任等级与业务优先级。
- 识别冲突证据;需要时保留双方并标记时间和来源,而不是静默覆盖。
- 为固定指令、当前请求、证据、记忆、工具 schema 和输出建立预算桶。
- 按消息、段落、JSON 对象等原子单元选择,避免中途硬切。
- 生成不可变快照,记录 selected IDs、dropped reasons、估算 token 和版本。
- 调用后记录实际 usage、响应状态和引用,供评测与审计。
同样输入应尽量得到确定性的装配结果:比较器要有稳定 tie-breaker,缓存读取要固定版本,异步返回顺序不能直接决定上下文顺序。
面试追问:ACL 应在向量检索前还是后? 最好由检索系统在查询时过滤,以避免越权候选进入结果与缓存;应用层仍可做防御性复核。若底层无法安全过滤,不能简单“先全量召回再让模型忽略”。
常见错误回答:“先按相关度取 top-k,再把 tenantId 写进 prompt”“候选列表顺序无所谓”。
Q5:上下文 token 预算应该如何分配?
**核心回答:**预算不是“窗口减去用户文本后全部给 RAG”,而是多个有优先级的桶。必须先为输出和协议开销留余量,再分配输入。
一个常见模型是:
总窗口 ≥ 固定指令 + 当前请求 + 工具定义 + 会话状态 + 检索证据 + 协议开销 + 最大输出
固定安全策略与当前请求优先;证据按价值选择;旧对话和重复示例通常更早被压缩或丢弃。输出预算过小会导致 JSON 或代码被截断。若供应商不公开精确聊天包装 token,应使用保守余量,并用实际 usage 校准。
预算策略应按任务配置:分类任务可能需要极少输出;代码审查需要较大输入和输出;工具 schema 很大时应只暴露当前任务允许的工具,而不是把所有工具定义都塞入。
面试追问:超限时为什么不直接截最后 N 个字符? 字符与 token 不对应,且会破坏 UTF-16、JSON、SQL、代码块或证据语义。应按原子项丢弃、重新检索较小片段,或使用可回源的压缩表示。
常见错误回答:“模型窗口是输入上限,输出另算”“工具定义不占上下文”。精确定义取决于 API,但工程预算不能假设它们免费。
Q6:相关性、时效性、权限和信任等级如何共同排序?
**核心回答:**权限是硬过滤,不是参与排序的软分数;无权数据即使相关度最高也必须删除。通过 ACL 后,才结合相关性、时效性、来源权威性、业务优先级与重复度排序。
可以使用可解释的线性分数作为起点:
score = wᵣ × relevance + wₜ × freshness + wₐ × authority + wᵦ × businessPriority
但分数不应掩盖硬规则,例如已撤销文档必须过滤,过期政策不能因为向量相似就排第一。不同特征量纲需要归一化,权重通过标注集和线上实验确定。
冲突不是简单取最高分。合同、价格、权限等高风险领域应优先选择正式系统的当前版本,保留冲突告警并允许回源。模型不应自行猜测哪个事实生效。
面试追问:怎样去重? 可按稳定资源 ID + 版本精确去重,再用内容 hash 或近重复算法合并重叠片段;合并时保留全部来源映射,避免引用丢失。
常见错误回答:“把 ACL 结果作为一个较低权重加入 score”“向量相似度最高的内容就是事实”。
Q7:为什么长上下文可能降低质量?
**核心回答:**更多 token 会引入噪声、重复和冲突,并增加模型在长序列中定位关键信息的难度。研究表明,模型在长上下文不同位置使用信息的能力可能不均匀,但具体表现依模型、任务和版本而异,不能把某个位置规律当作普适公式。
关键指令和当前请求应按目标模型建议放置,证据应有标题、来源和清晰边界;不过调整位置只是质量优化,不是安全控制。需要用任务级评测比较不同长度、排序和切分策略,而不是只验证是否“塞得下”。
长上下文还有系统成本:prefill 延迟、KV Cache、计费、缓存压力和日志治理。把 100 个低价值片段换成 8 个有权且互补的片段,往往更便于引用和诊断。
面试追问:top-k 是否固定为 5 或 10? 不应机械固定。文档粒度、问题类型、重排效果和窗口预算不同,应在离线召回/回答评测与线上成本之间选择。
常见错误回答:“上下文越长准确率单调上升”“把关键内容复制三遍就一定更受重视”。
Q8:上下文压缩有哪些策略,摘要为何不是无损压缩?
**核心回答:**常见策略包括抽取关键段、结构化状态、生成式摘要、滑动窗口、层次摘要和按需回源。摘要可能遗漏、改写甚至引入原文没有的陈述,因此必须保留来源、摘要器版本和回源入口。
| 策略 | 优点 | 主要边界 |
|---|---|---|
| 抽取原句/原段 | 可追溯、较少改写 | 仍可能较长,缺少跨段整合 |
| 结构化状态 | 字段稳定、易校验 | schema 需演进,无法覆盖所有语义 |
| 生成式摘要 | 压缩率高、可融合 | 非无损,可能遗漏或产生新陈述 |
| 滑动窗口 | 简单、适合短会话 | 早期关键约束可能丢失 |
| 检索回原文 | 只加载当前需要内容 | 依赖索引质量和稳定资源 ID |
金额、权限、合同条款、代码补丁、审计事实不应只依赖生成式摘要。可将会话目标和已完成步骤存为结构化状态,将高风险事实保留为 ID 和版本,回答时重新从权威系统读取。
面试追问:摘要如何评测? 检查关键事实保留率、无来源新增陈述、数字/实体准确性和回源成功率;不能只让另一个模型评价“读起来不错”。
常见错误回答:“摘要只是删除冗余,不会改变事实”“压缩后原文可以立即删除”。
Q9:会话记忆应该保存什么,不应该保存什么?
**核心回答:**记忆应服务于明确任务,并区分短期会话状态、长期用户偏好和权威业务事实。不要把全部聊天记录永久保存,也不要把模型总结当作业务数据库。
适合结构化保存的内容包括用户明确同意的偏好、当前任务 ID、已完成步骤和待确认事项。账户余额、权限、订单状态等应在需要时从权威系统读取。长期记忆要有用途限制、保留期限、删除机制、访问控制和用户可见性,遵循组织的数据治理与隐私要求。
记忆写入同样是不可信输出处理:模型提取的“用户偏好”需要置信度、证据和必要时的用户确认,不能因为一句玩笑就永久改变配置。
面试追问:如何避免旧记忆污染? 记录
validFrom/validTo、来源和版本,使用时检查新鲜度;冲突时以用户最新明确设置或权威系统为准,并允许撤销。
常见错误回答:“向量库就是长期记忆,所有对话都写进去”“只要加密就可以无限期保存”。
Q10:Few-shot 示例应如何选择和版本化?
**核心回答:**Few-shot 用少量输入—输出示例展示分类边界、格式或风格,适合补充自然语言难以精确描述的语义判断。示例应覆盖高频边界、负例和“信息不足”,而不是堆叠相似成功案例。
示例是生产配置:要有独立 exampleSetVersion,与模板、schema 和评测集一起变更;每次发布比较准确率、拒答率、token 成本和不同切片表现。动态选择示例时,也要执行 ACL 和防注入处理,不能从其他租户历史中检索案例。
强结构化任务优先使用 API 明确支持的 schema/工具协议,并在服务端校验;few-shot 只能改善遵循概率和语义示范,不能保证 JSON 合法。
面试追问:示例越多越好吗? 不。示例消耗 token,可能相互冲突、固化过期规则或让模型过拟合表面模式;应通过消融实验确定边际收益。
常见错误回答:“few-shot 是微调,不占上下文”“只放正例即可”。
Q11:Prompt caching 如何设计,为什么会有隔离风险?
**核心回答:**一些推理平台可以复用稳定前缀的计算结果,以降低重复 prefill 成本;是否支持、命中规则、计费和有效期均依供应商实现。应用不能假设所有 API 都有相同缓存语义。
为了提高命中,可把稳定的系统模板和工具定义放在前部,把每次变化的用户数据放后部。但缓存键至少要考虑模型/快照、模板版本、工具 schema 版本、内容版本以及供应商要求。若应用自己缓存已装配上下文,还必须包含租户、主体或授权范围;否则可能把 A 用户有权读取的内容返回给 B 用户。
权限变更和文档撤销要求缓存可失效。只使用 prompt hash 不能代表调用者仍有权访问其中数据。敏感内容还要确认供应商的数据保留和缓存政策是否符合组织要求。
面试追问:缓存命中后是否可跳过 ACL? 不可以。缓存是性能层,授权应基于当前主体和当前策略重新判断;缓存键只是辅助隔离,不是授权证明。
常见错误回答:“相同文本 hash 就能跨用户共享”“prompt cache 命中意味着模型输出也相同”。
Q12:如何让一次上下文装配可复现、可评测?
**核心回答:**保存足够的不可变元数据,使团队能区分“没取到、被权限过滤、预算裁掉、模型没使用、输出解析失败”这些不同问题。
建议记录:
- 请求/trace ID、任务类型、租户与脱敏主体标识;
- 模型 ID 或可用快照、采样参数、模板版本、example set 版本;
- 查询改写版本、检索参数、候选资源 ID + 版本 + 分数;
- ACL 策略版本、selected IDs、dropped reasons、稳定排序结果;
- 输入 token 估算、实际 usage、输出预留、上下文快照 hash;
- 工具 schema 版本、工具调用与最终响应状态;
- 质量标签、引用正确性、安全回归结果和成本。
若供应商只提供滚动模型别名,就不能承诺完全复现权重;应保存实际可获得的 model 标识和响应元数据,并把不可复现性写入诊断边界。日志本身包含敏感上下文,应最小化、脱敏、加密并限制保留期限。
面试追问:线上指标只看回答点赞率够吗? 不够。还要看无答案正确拒绝、引用命中、越权率(目标应为零)、裁剪率、来源缺失率、token 成本、延迟和按租户/语言/任务的切片。
常见错误回答:“保存最终 prompt 文本就能完全复现”“为了排障应永久记录所有原始数据”。
Java 21 完整示例:确定性的上下文装配器
示例仅使用 Java 21 标准库。tokenizer 被抽象为 TokenEstimator 接口,不假设任何模型 SDK。生产系统应由目标 tokenizer 的适配器实现,并使用实际 usage 校准。
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public final class ContextAssemblerDemo {
enum TrustLevel { AUTHORITATIVE, INTERNAL, EXTERNAL }
record Principal(String tenantId, Set<String> readableResourceIds) {
Principal {
Objects.requireNonNull(tenantId);
readableResourceIds = Set.copyOf(readableResourceIds);
}
}
record ContextItem(
String id,
String resourceId,
String tenantId,
long version,
String content,
TrustLevel trustLevel,
double relevance,
Instant validUntil
) {
ContextItem {
Objects.requireNonNull(id);
Objects.requireNonNull(resourceId);
Objects.requireNonNull(tenantId);
Objects.requireNonNull(content);
Objects.requireNonNull(trustLevel);
Objects.requireNonNull(validUntil);
if (version < 0) throw new IllegalArgumentException("version < 0");
if (relevance < 0.0 || relevance > 1.0) {
throw new IllegalArgumentException("relevance must be in [0, 1]");
}
}
}
interface TokenEstimator {
int estimate(String text);
}
record Dropped(String itemId, String reason) {}
record Assembly(List<ContextItem> selected, List<Dropped> dropped,
int estimatedTokens, int remainingTokens) {
Assembly {
selected = List.copyOf(selected);
dropped = List.copyOf(dropped);
}
}
static final class ContextAssembler {
private static final Comparator<ContextItem> ORDER =
Comparator.comparing(ContextItem::trustLevel)
.thenComparing(ContextItem::relevance, Comparator.reverseOrder())
.thenComparing(ContextItem::version, Comparator.reverseOrder())
.thenComparing(ContextItem::id);
private final TokenEstimator tokenEstimator;
private final int contextWindow;
private final int fixedPromptTokens;
private final int outputReserve;
private final int protocolReserve;
ContextAssembler(TokenEstimator tokenEstimator, int contextWindow,
int fixedPromptTokens, int outputReserve, int protocolReserve) {
this.tokenEstimator = Objects.requireNonNull(tokenEstimator);
this.contextWindow = contextWindow;
this.fixedPromptTokens = fixedPromptTokens;
this.outputReserve = outputReserve;
this.protocolReserve = protocolReserve;
}
Assembly assemble(Principal principal, String userQuestion,
List<ContextItem> candidates, Instant now) {
int baseTokens = fixedPromptTokens + outputReserve + protocolReserve
+ tokenEstimator.estimate(userQuestion);
int available = contextWindow - baseTokens;
if (available < 0) {
throw new IllegalArgumentException("固定内容与输出预留已超过上下文窗口");
}
List<Dropped> dropped = new ArrayList<>();
Map<String, List<ContextItem>> newestByResource = new LinkedHashMap<>();
candidates.stream()
.sorted(Comparator.comparing(ContextItem::id))
.forEach(item -> {
if (!item.tenantId().equals(principal.tenantId())
|| !principal.readableResourceIds().contains(item.resourceId())) {
dropped.add(new Dropped(item.id(), "ACL_DENIED"));
} else if (item.validUntil().isBefore(now)) {
dropped.add(new Dropped(item.id(), "EXPIRED"));
} else {
List<ContextItem> currentChunks = newestByResource.get(item.resourceId());
if (currentChunks == null) {
newestByResource.put(item.resourceId(),
new ArrayList<>(List.of(item)));
} else {
long currentVersion = currentChunks.getFirst().version();
if (item.version() > currentVersion) {
currentChunks.forEach(old ->
dropped.add(new Dropped(old.id(), "OLDER_RESOURCE_VERSION")));
newestByResource.put(item.resourceId(),
new ArrayList<>(List.of(item)));
} else if (item.version() == currentVersion
&& currentChunks.stream().noneMatch(old -> old.id().equals(item.id()))) {
// 同一资源的最新版本可以合法包含多个不同 chunk。
currentChunks.add(item);
} else {
dropped.add(new Dropped(item.id(),
item.version() < currentVersion
? "OLDER_RESOURCE_VERSION"
: "DUPLICATE_ITEM"));
}
}
}
});
List<ContextItem> eligible = newestByResource.values().stream()
.flatMap(List::stream)
.sorted(ORDER)
.toList();
List<ContextItem> selected = new ArrayList<>();
int used = 0;
for (ContextItem item : eligible) {
int cost = tokenEstimator.estimate(item.content());
if (cost <= available - used) {
selected.add(item);
used += cost;
} else {
dropped.add(new Dropped(item.id(), "TOKEN_BUDGET"));
}
}
return new Assembly(selected, dropped, baseTokens + used, available - used);
}
}
public static void main(String[] args) {
// 演示估算器仅用于让示例可运行,不代表任何真实模型 tokenizer。
TokenEstimator conservativeEstimator = text -> Math.max(1, text.codePointCount(0, text.length()));
var assembler = new ContextAssembler(conservativeEstimator, 200, 20, 60, 10);
var principal = new Principal("tenant-a", Set.of("policy", "runbook"));
Instant now = Instant.parse("2026-08-16T00:00:00Z");
List<ContextItem> candidates = List.of(
new ContextItem("p-v2", "policy", "tenant-a", 2,
"退款超过一万元必须由财务复核。", TrustLevel.AUTHORITATIVE,
0.95, now.plusSeconds(3_600)),
new ContextItem("p-v1", "policy", "tenant-a", 1,
"旧版退款流程。", TrustLevel.AUTHORITATIVE,
0.90, now.plusSeconds(3_600)),
new ContextItem("secret", "other", "tenant-b", 7,
"其他租户数据。", TrustLevel.INTERNAL,
0.99, now.plusSeconds(3_600)),
new ContextItem("r-v3", "runbook", "tenant-a", 3,
"复核失败时进入人工工单队列。", TrustLevel.INTERNAL,
0.85, now.plusSeconds(3_600))
);
Assembly result = assembler.assemble(principal, "大额退款如何处理?", candidates, now);
System.out.println("selected=" + result.selected().stream().map(ContextItem::id).toList());
System.out.println("dropped=" + result.dropped());
System.out.println("estimatedTokens=" + result.estimatedTokens());
}
}**复杂度与成本模型:**设候选数为 n,排序为 O(n log n);ACL、过期检查、去重与预算选择为 O(n);额外空间为 O(n)。token 估算成本还与总文本长度 L 有关,通常至少需要 O(L) 扫描。示例按 TrustLevel 枚举顺序排序(权威来源优先),再按相关性、版本和 ID 稳定排序;实际权重必须通过评测确定。
**工程边界:**示例把 ACL 做在应用层是为了展示不变量,真实系统还应在检索查询阶段过滤;readableResourceIds 不适合海量 ACL,应由授权服务或检索引擎的过滤能力承载。示例不截断单个 item,因此可能留下未使用预算,这是用容量换结构完整性。若要切片,应在索引阶段生成可追溯的原子块,而不是装配时硬切字符串。
场景设计题
场景:设计一个能读取内部文档并创建工单的企业 Agent 上下文层
要求:多租户;文档来自 Wiki、邮件和工单;允许只读检索和“创建工单”工具;高风险操作需确认;需要复现一次错误回答。
推荐设计:
- 信任与权限模型:每个片段携带 tenant、resource ID、版本、ACL、来源、有效期和内容 hash;检索时强制按主体过滤。
- 数据入口治理:解析器不执行附件宏或脚本;外部内容标记为不可信数据;同步链路保留原始来源和删除事件。
- 装配策略:按任务选择最少工具;固定规则、当前请求、证据和输出分别分桶;去重、稳定排序、按原子块裁剪。
- 注入防线:文档中的指令不改变工具权限;“创建工单”参数只允许 schema 中字段,tenant/user 从服务端会话派生;目标项目也必须鉴权。
- 高风险确认:模型先生成可审阅计划,展示目标、摘要和引用;用户确认后服务端重新鉴权并执行,使用幂等键。
- 记忆边界:只保存任务状态和经确认的偏好;工单真实状态每次从业务系统读取,不依赖会话摘要。
- 可复现性:保存模板/example/schema 版本、模型标识、候选与选中资源版本、ACL 策略版本、裁剪原因、工具调用和 usage;敏感正文按治理要求最小化保存。
- 评测:构建正常问答、无答案、冲突文档、过期文档、跨租户、直接/间接注入、工具越权和确认绕过测试集;越权测试必须阻断于模型外。
**加分点:**指出引用正确不等于结论正确,仍需检查证据是否真正蕴含回答;模型拒绝执行恶意指令是质量加分,但即使模型失守,工具网关也必须拒绝越权调用。
速记总结
- Prompt 工程设计表达;上下文工程管理进入模型的信息、权限、预算、版本和生命周期。
- 指令层级依目标 API 而异,是行为约定,不是认证授权边界。
- 外部文本都应视为不可信数据;分隔符和注入检测只能减风险,不能替代工具侧强制控制。
- 装配顺序应是先认证和 ACL,再相关性排序;无权数据不能以低分形式进入模型。
- 上下文预算必须包括指令、历史、RAG、工具 schema、协议开销和输出预留。
- 按消息/文档/JSON 对象等原子单元裁剪,避免硬切字符串破坏语义与结构。
- 长窗口不保证质量;噪声、冲突、重复、位置和成本都要用任务评测验证。
- 生成式摘要不是无损压缩;高风险事实保留来源、版本并按需回源。
- 记忆不是业务数据库;只保存必要、可撤销、有期限且经授权的信息。
- Prompt cache 是性能优化,不是授权证明;缓存键、失效和租户隔离必须设计。
- 可复现性来自版本化快照与装配决策,不只是保存最终 prompt 文本。
- 最终安全边界在模型外:最小权限、参数约束、重新鉴权、幂等、确认和审计。
参考资料
- OWASP GenAI Security Project:Prompt Injection
- OWASP:LLM Prompt Injection Prevention Cheat Sheet
- NIST AI Risk Management Framework 1.0
- NIST AI 600-1:Generative Artificial Intelligence Profile
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks
- Lost in the Middle: How Language Models Use Long Contexts
- Anthropic:Prompt engineering overview
- OpenAI Model Spec