上一篇我们把可观测性立起来了:streamEvents、LangSmith、结构化日志。出了错,你至少能看见「卡在哪一步」。
但说句扎心的:trace 再漂亮,也救不了窗口里塞的是垃圾 。历史消息、RAG 片段、ToolMessage 一股脑堆进去------要么超限直接报错,要么噪声淹没关键句,模型一本正经地胡话。可观测性回答「发生了什么 」;上下文工程(Context Engineering) 回答「模型看到了什么」------这才直接决定它能不能推对。
这一篇把 Context Engineering 和 Prompt Engineering 掰开,讲清一次调用里上下文怎么组成、Token 怎么预算,以及 RAG 注入时怎么少塞垃圾。
老规矩,本文以官网最新文档核对过(Context engineering in agents、Short-term memory、Prebuilt middleware)。Agent 入口继续用
createAgent------别再抄createReactAgent。网上不少教程手写一个同名trimMessages------别这么干 ,官方就有trimMessages;摘要优先走summarizationMiddleware。

一、Prompt Engineering ≠ Context Engineering
PromptTemplate / ChatPromptTemplate------那是 Prompt Engineering:把单条指令写清楚、格式对、few-shot 到位。
对话一变长、接上 RAG、再套多轮 tool 调用,上下文窗口就成了稀缺资源。这时你优化的不再是「这句话怎么措辞」,而是「这一整窗里放什么、什么顺序、超了怎么砍」------这就是 Context Engineering。
| 维度 | Prompt Engineering | Context Engineering |
|---|---|---|
| 关注点 | 单条 prompt 的措辞、格式、few-shot | 整段上下文的组成、顺序、长度与质量 |
| 范围 | 通常 system + 当前 user | system + 历史 + 检索片段 + 工具结果 + 元数据 |
| 目标 | 让模型「理解任务」 | 让模型「在有限窗口内看到最相关信息」 |
官网说得更狠一点:Agent 不可靠,往往不是模型不够聪明,而是没把「对的」上下文喂进去。AI Engineer 的头号工作,就是这件事。
二、一次调用里模型到底看到什么
先用落地直觉拆开------一次 Agent 调用的 context,通常长这样:
#mermaid-svg-b3c1beLJncSaaXYy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-b3c1beLJncSaaXYy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-b3c1beLJncSaaXYy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-b3c1beLJncSaaXYy .error-icon{fill:#552222;}#mermaid-svg-b3c1beLJncSaaXYy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-b3c1beLJncSaaXYy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-b3c1beLJncSaaXYy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-b3c1beLJncSaaXYy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-b3c1beLJncSaaXYy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-b3c1beLJncSaaXYy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-b3c1beLJncSaaXYy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-b3c1beLJncSaaXYy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-b3c1beLJncSaaXYy .marker.cross{stroke:#333333;}#mermaid-svg-b3c1beLJncSaaXYy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-b3c1beLJncSaaXYy p{margin:0;}#mermaid-svg-b3c1beLJncSaaXYy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-b3c1beLJncSaaXYy .cluster-label text{fill:#333;}#mermaid-svg-b3c1beLJncSaaXYy .cluster-label span{color:#333;}#mermaid-svg-b3c1beLJncSaaXYy .cluster-label span p{background-color:transparent;}#mermaid-svg-b3c1beLJncSaaXYy .label text,#mermaid-svg-b3c1beLJncSaaXYy span{fill:#333;color:#333;}#mermaid-svg-b3c1beLJncSaaXYy .node rect,#mermaid-svg-b3c1beLJncSaaXYy .node circle,#mermaid-svg-b3c1beLJncSaaXYy .node ellipse,#mermaid-svg-b3c1beLJncSaaXYy .node polygon,#mermaid-svg-b3c1beLJncSaaXYy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-b3c1beLJncSaaXYy .rough-node .label text,#mermaid-svg-b3c1beLJncSaaXYy .node .label text,#mermaid-svg-b3c1beLJncSaaXYy .image-shape .label,#mermaid-svg-b3c1beLJncSaaXYy .icon-shape .label{text-anchor:middle;}#mermaid-svg-b3c1beLJncSaaXYy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-b3c1beLJncSaaXYy .rough-node .label,#mermaid-svg-b3c1beLJncSaaXYy .node .label,#mermaid-svg-b3c1beLJncSaaXYy .image-shape .label,#mermaid-svg-b3c1beLJncSaaXYy .icon-shape .label{text-align:center;}#mermaid-svg-b3c1beLJncSaaXYy .node.clickable{cursor:pointer;}#mermaid-svg-b3c1beLJncSaaXYy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-b3c1beLJncSaaXYy .arrowheadPath{fill:#333333;}#mermaid-svg-b3c1beLJncSaaXYy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-b3c1beLJncSaaXYy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-b3c1beLJncSaaXYy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b3c1beLJncSaaXYy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-b3c1beLJncSaaXYy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b3c1beLJncSaaXYy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-b3c1beLJncSaaXYy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-b3c1beLJncSaaXYy .cluster text{fill:#333;}#mermaid-svg-b3c1beLJncSaaXYy .cluster span{color:#333;}#mermaid-svg-b3c1beLJncSaaXYy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-b3c1beLJncSaaXYy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-b3c1beLJncSaaXYy rect.text{fill:none;stroke-width:0;}#mermaid-svg-b3c1beLJncSaaXYy .icon-shape,#mermaid-svg-b3c1beLJncSaaXYy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b3c1beLJncSaaXYy .icon-shape p,#mermaid-svg-b3c1beLJncSaaXYy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-b3c1beLJncSaaXYy .icon-shape .label rect,#mermaid-svg-b3c1beLJncSaaXYy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b3c1beLJncSaaXYy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-b3c1beLJncSaaXYy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-b3c1beLJncSaaXYy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Context 组成
System Prompt
历史 Messages
RAG 检索片段
Tool 结果 ToolMessage
当前 User Message
| 部分 | 来源 | 说明 |
|---|---|---|
| System Prompt | 固定或动态 | 角色、规则、工具使用约定 |
| 历史 Messages | Checkpointer / Memory | 多轮对话累积 |
| RAG 片段 | Retriever Top-K | 注入 prompt 的参考文档 |
| Tool 结果 | ToolMessage |
ReAct 环里每次 tool 返回 |
| 当前 User Message | 用户输入 | 本轮问题 |
官网再给你一层更完整的坐标系------你能控的不只是「消息列表」,而是三类上下文:
| Context Type | 你在控什么 | Transient / Persistent |
|---|---|---|
| Model Context | 进模型的东西:instructions、message history、tools、用哪颗模型、response format | 多为 Transient(只改本轮喂给模型的内容) |
| Tool Context | 工具能读什么、写什么(State / Store / Runtime Context) | Persistent |
| Life-cycle Context | 模型调用与工具调用之间发生什么(摘要、guardrails、日志......) | Persistent |
数据从哪来,也要分清:
| 数据源 | 范围 | 例子 |
|---|---|---|
| Runtime Context | 单次会话配置 | userId、权限、环境 |
| State(短期记忆) | 当前 thread | messages、tool 结果、上传文件 |
| Store(长期记忆) | 跨会话 | 用户偏好、沉淀事实 |
落地机制是 Middleware。 createAgent 的 middleware 让你在 agent loop 的钩子上改上下文------不必把裁剪逻辑糊进业务节点。
#mermaid-svg-UuJdrSzlOJa4SCi6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UuJdrSzlOJa4SCi6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UuJdrSzlOJa4SCi6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UuJdrSzlOJa4SCi6 .error-icon{fill:#552222;}#mermaid-svg-UuJdrSzlOJa4SCi6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UuJdrSzlOJa4SCi6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UuJdrSzlOJa4SCi6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UuJdrSzlOJa4SCi6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UuJdrSzlOJa4SCi6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UuJdrSzlOJa4SCi6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UuJdrSzlOJa4SCi6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UuJdrSzlOJa4SCi6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UuJdrSzlOJa4SCi6 .marker.cross{stroke:#333333;}#mermaid-svg-UuJdrSzlOJa4SCi6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UuJdrSzlOJa4SCi6 p{margin:0;}#mermaid-svg-UuJdrSzlOJa4SCi6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UuJdrSzlOJa4SCi6 .cluster-label text{fill:#333;}#mermaid-svg-UuJdrSzlOJa4SCi6 .cluster-label span{color:#333;}#mermaid-svg-UuJdrSzlOJa4SCi6 .cluster-label span p{background-color:transparent;}#mermaid-svg-UuJdrSzlOJa4SCi6 .label text,#mermaid-svg-UuJdrSzlOJa4SCi6 span{fill:#333;color:#333;}#mermaid-svg-UuJdrSzlOJa4SCi6 .node rect,#mermaid-svg-UuJdrSzlOJa4SCi6 .node circle,#mermaid-svg-UuJdrSzlOJa4SCi6 .node ellipse,#mermaid-svg-UuJdrSzlOJa4SCi6 .node polygon,#mermaid-svg-UuJdrSzlOJa4SCi6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UuJdrSzlOJa4SCi6 .rough-node .label text,#mermaid-svg-UuJdrSzlOJa4SCi6 .node .label text,#mermaid-svg-UuJdrSzlOJa4SCi6 .image-shape .label,#mermaid-svg-UuJdrSzlOJa4SCi6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-UuJdrSzlOJa4SCi6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UuJdrSzlOJa4SCi6 .rough-node .label,#mermaid-svg-UuJdrSzlOJa4SCi6 .node .label,#mermaid-svg-UuJdrSzlOJa4SCi6 .image-shape .label,#mermaid-svg-UuJdrSzlOJa4SCi6 .icon-shape .label{text-align:center;}#mermaid-svg-UuJdrSzlOJa4SCi6 .node.clickable{cursor:pointer;}#mermaid-svg-UuJdrSzlOJa4SCi6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UuJdrSzlOJa4SCi6 .arrowheadPath{fill:#333333;}#mermaid-svg-UuJdrSzlOJa4SCi6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UuJdrSzlOJa4SCi6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UuJdrSzlOJa4SCi6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UuJdrSzlOJa4SCi6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UuJdrSzlOJa4SCi6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UuJdrSzlOJa4SCi6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UuJdrSzlOJa4SCi6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UuJdrSzlOJa4SCi6 .cluster text{fill:#333;}#mermaid-svg-UuJdrSzlOJa4SCi6 .cluster span{color:#333;}#mermaid-svg-UuJdrSzlOJa4SCi6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UuJdrSzlOJa4SCi6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UuJdrSzlOJa4SCi6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-UuJdrSzlOJa4SCi6 .icon-shape,#mermaid-svg-UuJdrSzlOJa4SCi6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UuJdrSzlOJa4SCi6 .icon-shape p,#mermaid-svg-UuJdrSzlOJa4SCi6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UuJdrSzlOJa4SCi6 .icon-shape .label rect,#mermaid-svg-UuJdrSzlOJa4SCi6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UuJdrSzlOJa4SCi6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UuJdrSzlOJa4SCi6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UuJdrSzlOJa4SCi6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} tool_calls
done
start
beforeModel
Model Call
afterModel
Tools
end
wrapModelCall:改本轮送给模型的 messages / tools / prompt------瞬时,默认不改 State。beforeModel/afterModel:可以返回 State 更新(例如删消息、换摘要)------持久,Checkpointer 下次还能看见。
搞不清 Transient vs Persistent,你就会踩这个坑:以为「裁过了」,结果 Checkpointer 里旧历史还在,下一轮又全塞回来。
三、Token 预算:三种控窗策略
模型窗口有限(本地小模型尤其狠)。管理原则很简单:给每块预算,超限有明确裁剪 / 压缩规则。
1. 保留最近 N 轮
只留最近几轮 user-assistant,更早的直接丢掉。实现最简单,适合短会话、demo。
别手写一个叫 trimMessages 的函数去抢官方名字------下面用官网 API。
2. 官方 trimMessages:按 token / 边界裁剪
LangChain 提供 trimMessages:按 maxTokens、strategy: "last"、startOn / endOn 裁消息列表,尽量保住对话结构(例如从 human 起、在 human/tool 结束,避免 AI↔Tool 成对被拦腰砍断)。
瞬时裁剪 (只改本轮喂给模型的内容,State 原样保留)------用 wrapModelCall:
typescript
import { createAgent, createMiddleware, trimMessages } from "langchain";
import { ChatOllama } from "@langchain/ollama";
import { tool } from "@langchain/core/tools";
import * as z from "zod";
const getWeather = tool(
async ({ city }: { city: string }) => `${city}:晴,25°C`,
{
name: "get_weather",
description: "查询城市天气",
schema: z.object({ city: z.string() }),
}
);
/** 教学用:按「条数」近似计数;生产请换成真实 tokenCounter(或模型自带计数) */
const roughCounter = (msgs: { length: number } | unknown[]) =>
Array.isArray(msgs) ? msgs.length : 0;
const transientTrim = createMiddleware({
name: "TransientTrim",
wrapModelCall: async (request, handler) => {
const trimmed = await trimMessages(request.messages, {
maxTokens: 12, // 演示阈值;生产按模型窗口设
strategy: "last",
startOn: "human",
endOn: ["human", "tool"],
includeSystem: true, // 保住开头的 system
tokenCounter: roughCounter,
});
// override:只改本轮请求,不写回 State
return handler(request.override({ messages: trimmed }));
},
});
const llm = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });
const agent = createAgent({
model: llm,
tools: [getWeather],
systemPrompt: "需要天气时调用 get_weather。回答简洁。",
middleware: [transientTrim],
});
持久裁剪 (真把 State 里的旧消息清掉,配合 Checkpointer 才有意义)------beforeModel + RemoveMessage:
typescript
import { RemoveMessage } from "@langchain/core/messages";
import { createAgent, createMiddleware, trimMessages } from "langchain";
import { MemorySaver, REMOVE_ALL_MESSAGES } from "@langchain/langgraph";
import { ChatOllama } from "@langchain/ollama";
const persistTrim = createMiddleware({
name: "PersistTrim",
beforeModel: async (state) => {
const trimmed = await trimMessages(state.messages, {
maxTokens: 20,
strategy: "last",
startOn: "human",
endOn: ["human", "tool"],
includeSystem: true,
tokenCounter: (msgs) => msgs.length, // 演示用;生产换真实计数
});
// 先清空再写入裁剪结果 → State 永久变短
return {
messages: [new RemoveMessage({ id: REMOVE_ALL_MESSAGES }), ...trimmed],
};
},
});
const agent = createAgent({
model: new ChatOllama({ model: "qwen2.5:7b", temperature: 0 }),
tools: [],
middleware: [persistTrim],
checkpointer: new MemorySaver(),
});
// 同一 thread_id 多轮 invoke:裁剪结果会跟着存档走
await agent.invoke(
{ messages: [{ role: "user", content: "我叫小明" }] },
{ configurable: { thread_id: "u-1" } }
);
| 策略 | 改 State? | 适合 |
|---|---|---|
wrapModelCall + trimMessages |
否(Transient) | 调试、按调用临时瘦身、还想保留完整审计历史 |
beforeModel + RemoveMessage |
是(Persistent) | Checkpointer 长会话,必须真的减负 |
summarizationMiddleware |
是(Persistent) | 长对话要「记得大概」,不能硬砍细节 |
3. 摘要压缩:summarizationMiddleware
硬 trim 会丢信息。长会话更常见的做法:旧消息用另一颗(可更小更便宜的)模型压成摘要,永久写回 State,只保留最近若干条原文。
typescript
import { createAgent, summarizationMiddleware } from "langchain";
import { ChatOllama } from "@langchain/ollama";
import { MemorySaver } from "@langchain/langgraph";
const chatModel = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });
// 摘要可以用同一模型,生产常换成更小/更便宜的
const summaryModel = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });
const agent = createAgent({
model: chatModel,
tools: [],
checkpointer: new MemorySaver(),
middleware: [
summarizationMiddleware({
model: summaryModel,
trigger: { tokens: 4000 }, // 越过阈值才摘要
keep: { messages: 20 }, // 保留最近 20 条原文
}),
],
});
触发条件还可写成「多条件 AND」或「数组 OR」(见官网 Prebuilt middleware)。注意:摘要是文本向压缩------多模态大图不会被「压小」,只会被摘要文字替代;图多的场景要把媒体放对象存储,消息里只留 URL。
四、RAG 怎么注入才不搅浑
这里只盯一件事:检索到的文档怎么塞进 prompt------格式不对,模型分不清「资料」和「问题」,引用也乱。
分隔符 + 引用格式
text
--- 检索到的参考文档 ---
[来源: doc-a.md]
......
--- 参考文档结束 ---
用户问题:......
并明确要求:答不出就说不知道;引用时标 [来源: xxx]。
typescript
import { Document } from "@langchain/core/documents";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOllama } from "@langchain/ollama";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnableSequence } from "@langchain/core/runnables";
/** 把检索文档格式化成带来源的 context */
function formatRagContext(docs: Document[]): string {
return docs
.map(
(d, i) =>
`[来源: ${d.metadata.source ?? `doc-${i}`}]\n${d.pageContent}`
)
.join("\n\n---\n\n");
}
const prompt = ChatPromptTemplate.fromTemplate(
`根据以下参考文档回答问题。若无法从文档得出答案,请说「我不知道」。
回答时请用 [来源: xxx] 标注引用。
--- 检索到的参考文档 ---
{context}
--- 参考文档结束 ---
用户问题:{question}`
);
const llm = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });
const ragChain = RunnableSequence.from([
async (input: { question: string; docs: Document[] }) => ({
context: formatRagContext(input.docs),
question: input.question,
}),
prompt,
llm,
new StringOutputParser(),
]);
// ragChain.invoke({ question: "...", docs: retrievedDocs })
Top-K 与 chunk 也是预算
| 旋钮 | 太大 | 建议起步 |
|---|---|---|
k |
噪声淹没相关句 | 3~5 |
chunkSize |
单条占满窗口 | 视文档类型:FAQ 可 200~300,论述可更大 |
多轮 + RAG + tool 结果三者叠加时,先给历史 / RAG / tools 各自定预算 ,再决定 trim 还是摘要------别等 API 报 context length exceeded 再救火。
工具结果特别脏、特别长时,还可以看官网的 contextEditingMiddleware(如 ClearToolUsesEdit):专门清旧 tool 调用块,避免 ToolMessage 永久占地。本篇不展开,知道有这号预置中间件即可。
五、反模式速查表
| 反模式 | 后果 | 缓解 |
|---|---|---|
| 塞满 context | 「迷失」在无关信息里,忽略关键句 | 预算 + trim / 摘要 |
| 检索噪声淹没相关信息 | 基于错误资料一本正经胡答 | 降 k、重排、分隔符、强制「无则不知」 |
| 历史从不裁剪 | 超限报错或被截断 | trimMessages / summarizationMiddleware |
| Tool 结果永不清理 | ToolMessage 挤占 user 问题空间 | 只留近几轮 tool;或 context editing |
| system prompt 过长 | 规则/工具说明占满窗口 | 精简 system;动态工具子集(官网 Tool Context) |
| 把瞬时 trim 当永久清理 | Checkpointer 下轮又全量塞回 | 长会话用 Persistent 策略 |
常见坑
- 只改 Prompt 不管理 context:长对话 + RAG 后必然爆窗。
- RAG 无分隔直接拼接:模型分不清文档与问题。
- trim 丢掉 system :角色与规则蒸发------设
includeSystem: true,或保证 system 始终在保留集里。 - 裁断 AI↔Tool 成对消息 :部分供应商会直接拒收非法历史------用
startOn/endOn保结构。 k过大:10+ chunk 塞满窗口,噪声赢相关。- 摘要当真理:细节会丢;关键事实该进 Store / 结构化记忆,别全靠一段 summary。
- 自写同名
trimMessages:和官方 API 撞车,后人难维护------用官网的。