Prompt工程与上下文管理实战:从模板到Agent Loop的演进
做 LLM 应用开发一段时间之后,我发现一个很有意思的现象:刚入门的时候,大家把精力全花在"怎么写好一条 Prompt"上------角色设定怎么写、Few-shot 怎么放、CoT 怎么引导。但真正上线之后,踩的坑几乎都跟 Prompt 本身没关系,而是跟上下文管理有关。
Token 超了怎么办?对话历史太长模型"忘"了早期内容怎么办?不同信息的优先级怎么排?这些问题才是生产级 LLM 应用的核心工程挑战。
今天就从我做 nvc-guide的实战经验出发,聊聊 Prompt 工程和上下文管理的那些事儿。
Prompt 工程:不是玄学,是工程
Prompt 的本质
LLM 本质上是一个条件概率模型------给定上文(Prompt),预测下一个 token 的概率分布。Prompt Engineering 的目标就是构造最优的上文条件,让模型的输出集中在我们期望的区域。
一条完整的 Prompt 通常由这几部分组成:
| 组成部分 | 作用 | 示例 |
|---|---|---|
| System Prompt | 定义角色、行为边界、输出格式 | "你是 NVC 评估专家" |
| User Prompt | 用户的具体任务指令 | "请评估以下对话" |
| Context | 上下文信息(RAG、历史对话、用户档案) | 检索到的知识库片段 |
| Output Format | 约束输出结构 | "以 JSON 格式输出" |
| Examples | Few-shot 示例 | 2-3 个评估案例 |
几个实战技巧
负面约束前置。这是我在项目里验证过最有效的技巧。LLM 的注意力机制对开头的 token 权重更高,所以"不要编造""不要乱调工具"这类约束要放在 System Prompt 的最前面,而不是藏在最后。
差:(在 Prompt 末尾)...另外,请不要编造内容。
好:(在 System Prompt 开头)
# 核心原则
1. 绝对不要编造用户没有提到的内容
2. 工具调用失败时如实告知,不要换工具继续调用
工具触发条件具象化。不要只说"搜索笔记时调用 wiki_search",而是列出具体的触发词------"我的笔记""之前的记录""搜索笔记"。模糊的描述会让模型误判,具象化的触发词能大幅减少错误调用。
任务分解。查询改写和最终问答用不同的 Prompt,而不是在一条 Prompt 里同时完成。每个子任务有专门的 Prompt 优化空间,效果比"一锅炖"好得多。
模板化管理
项目里我用 StringTemplate(.st 文件)管理所有 Prompt 模板,一共 16 个。每个模板对应一个专职 Agent,职责单一------观察教练只关注"引导用户区分观察和评论",不会混入感受、需求等内容。
模板里用 {变量名} 占位符,运行时通过 PromptBuilder 动态注入用户档案、上下文摘要等信息。这样做的好处是:改 Prompt 不用改代码,改 .st 文件就行,迭代速度很快。
java
public String buildSystemPrompt(Long userId, String contextSummary) {
String template = getOrLoadTemplate(); // double-checked locking 缓存
template = template.replace("{userProfileSummary}",
profileService.getUserProfilePrompt(userId));
template = template.replace("{contextSummary}",
contextSummary != null ? contextSummary : "");
template = template.replace("{currentTime}",
LocalDateTime.now().format(TIME_FORMATTER));
return template;
}
Token 管理:多轮对话的核心工程问题
LLM 是无状态的------每次 API 调用都是独立的请求/响应。所有需要 LLM "知道"的信息,都必须在每次调用时作为上下文显式传入。多轮对话中,Token 数量持续增长,带来三个问题:
- Token 溢出:超出上下文窗口限制,LLM 直接报错
- 注意力稀释:上下文过长,LLM 对早期信息的注意力下降
- 成本失控:输入 Token 直接影响 API 调用费用
三层压缩方案
我在项目里用了滑动窗口 + LLM 摘要压缩 + 降级截断的三层方案:
消息数 ≤ 20 → 直接返回所有消息(不压缩)
消息数 > 20 → 触发压缩
├── 早期消息(第 1 到 N-10 条)→ LLM 摘要(不超过 500 字)
├── 最近消息(最后 10 条)→ 保持不变
└── 摘要注入 System Prompt 的 {contextSummary} 占位符
为什么是 20 轮?20 轮对话约消耗 6000-8000 tokens,加上 System Prompt(1500 tokens)和用户画像(300 tokens),总计约 10K tokens,留出充足的输出空间。
为什么保留 10 轮而不是 5 轮?NVC 练习是深度对话------用户可能在最近 5 轮内连续探讨一个 NVC 步骤,只保留 5 轮可能丢失当前步骤的上下文。10 轮能覆盖用户最近 2-3 个步骤的讨论。
降级截断是兜底方案:LLM 摘要调用失败时,取前 5 条消息,每条截断到 200 字符。保证即使 LLM 服务不可用,对话也能继续。
java
private String generateSummary(List<MessageEntity> messages) {
try {
String summary = client.prompt().user(prompt).call().content();
if (summary != null && !summary.isBlank()) return summary.trim();
return buildFallbackSummary(messages);
} catch (Exception e) {
return buildFallbackSummary(messages); // 降级
}
}
摘要注入的位置也有讲究------注入 System Prompt 而不是消息列表。System Prompt 在 LLM 的注意力机制中权重最高,不会被用户消息"挤掉"。
上下文优先级:哪些内容优先放
Token 预算有限,不可能把所有信息都塞进去。这时候就需要排优先级。
黄金法则:指令 > 状态 > 知识 > 记忆
| 优先级 | 类型 | 典型内容 | 理由 |
|---|---|---|---|
| 最高 | 指令型 | System Prompt、行为规则 | 定义 LLM 行为,不可替代 |
| 高 | 状态型 | 用户画像、当前场景 | "我是谁、在什么场景"比"需要什么知识"更基础 |
| 中 | 知识型 | RAG 检索结果 | 按需检索的领域知识,辅助回复 |
| 低 | 记忆型 | 对话历史、反思记忆 | 最动态,放在消息区 |
这个顺序有两层依据:一是 LLM 的注意力机制------开头和结尾的信息权重最高(Lost in the Middle 效应),最稳定的指令放在最前;二是信息依赖关系------LLM 需要先知道"我是谁、在什么场景",才能正确使用 RAG 知识和对话历史。
缓解 Lost in the Middle
2023 年斯坦福的研究发现,LLM 对上下文中间部分的信息关注度显著低于开头和结尾。如果把 5 个 RAG 文档片段塞进去,模型对第 1 个和最后 1 个的关注度最高,中间的容易被忽略。
我的应对策略:
- 用标签标注各部分(
[用户档案]、[参考资料]),帮助 LLM 定位 - RAG 只取 Top-3,控制中间区域的长度
- 最重要的信息放在开头或结尾
三代工程范式:Prompt → Context → Loop
LLM 应用的工程范式经历了三代演进,理解这个演进脉络对做架构设计很有帮助。
Prompt Engineering(第一代)
核心思想是通过精心设计的指令模板引导 LLM 输出。关键技术包括角色设定、Few-shot、CoT、结构化输出约束。本质是静态的、一次性的指令设计。
Context Engineering(第二代)
核心思想是动态组装 LLM 所需的完整上下文。不再只关注 Prompt 本身的措辞,而是关注"给 LLM 看什么信息"------短期记忆、长期记忆、RAG 检索、结构化状态、反思记忆,这些都需要动态组装。
Prompt 的措辞差异对输出质量的影响已经越来越小(模型越来越强),但上下文的质量和相关性直接决定了 LLM 的表现上限。
Loop Engineering(第三代)
核心思想是让 LLM 在一个受控循环中自主决策和执行。这就是 Agent 的本质------LLM 从"被动回答者"变成了"主动决策者"。
while (turn < MAX_TURNS) {
response = callLLM(messages)
if (response.hasToolCalls()) {
results = executeTools(response.toolCalls) // Hook 链介入
messages.add(toolResults)
turn++
} else {
emit(response.content)
break
}
}
Loop 需要解决的关键问题:终止条件(最大轮数、超时)、Hook 链(限流、缓存、权限)、可观测性(Trace 埋点)、降级策略。
三层是包含关系
这三代不是替代关系,而是包含关系:
┌─────────────────────────────────────────┐
│ Loop Engineering │
│ ┌─────────────────────────────────────┐│
│ │ Context Engineering ││
│ │ ┌─────────────────────────────────┐││
│ │ │ Prompt Engineering │││
│ │ │ 16 个 .st 模板 │││
│ │ └─────────────────────────────────┘││
│ └─────────────────────────────────────┘│
└─────────────────────────────────────────┘
Prompt 是基础模板,Context 在 Prompt 之上动态组装完整信息,Loop 在 Context 之上构建自主执行循环。三者缺一不可。
小结
Prompt 工程不是"写好一句话"那么简单,它背后是一整套工程体系:模板化管理、动态注入、负面约束前置、任务分解。上下文管理才是生产级 LLM 应用的核心------Token 预算分配、优先级排序、压缩策略、Lost in the Middle 缓解,每一个都是工程问题。
从 Prompt 到 Context 再到 Loop,是 LLM 应用从"玩具"到"产品"的必经之路。理解这个演进脉络,才能在架构设计时做出正确的取舍。