1. 从一次终端报错说起为什么第二轮对话丢了 Context第一次跑 Hello-Agents 第 9 章的时候我在终端里盯着一段输出看了很久。第一轮模型回复得好好的第二轮发过去的请求里Context 字段居然是空的。不是报错不是超时就是安安静静地空了。那一瞬间我甚至怀疑是自己代码写漏了反复翻了两遍messages的拼接逻辑才意识到问题出在上下文工程的组装环节而不是模型本身。这件事让我重新理解了「上下文工程」这四个字的分量。很多人把它等同于提示词工程觉得无非是把话说清楚一点。但真正在 Agent 场景里跑起来你会发现提示词只是第一层真正决定一个多轮 Agent 能不能稳定工作的是每一轮往 LLM 里塞进去的那坨 Context 到底是怎么被构造、裁剪、传递和复用的。Hello-Agents 第 9 章讲的正是这套东西而我在终端里踩的这个坑恰好是这套机制最典型的一个失效点。这篇内容适合两类人看一类是刚开始接触 Agent 开发、准备跟着 Hello-Agents 动手跑一遍的另一类是已经写过几轮对话逻辑但总觉得模型「记性不好」「答非所问」想搞清楚上下文到底该怎么管的。我会从整体设计思路讲到具体实现再到我实际踩过的坑和排查方法尽量把每一步为什么这么做都讲透让你看完能直接对着自己的终端复现一遍。2. 上下文工程到底在解决什么问题2.1 提示词工程和上下文工程的边界在哪先把概念理清楚不然后面全是糊涂账。提示词工程关注的是「这一次我要怎么跟模型说」它优化的是单次输入的表达质量比如角色设定、任务描述、输出格式约束。上下文工程关注的是「这一轮我该让模型看到什么」它管理的是跨轮次、跨工具、跨记忆的信息流。打个比方提示词工程像是你写一封邮件时的措辞上下文工程像是你决定这封邮件里要附上哪些历史往来、哪些附件、哪些背景资料。邮件写得再好如果该附的合同没附对方照样没法办事。Agent 场景里模型每一轮能看到的全部信息构成了它的「工作记忆」而这个工作记忆是有限的、需要被主动管理的。Hello-Agents 第 9 章把这块单独拎出来讲我觉得是很有必要的。因为一旦你开始做多轮 Agent上下文就不再是「把历史消息拼起来」这么简单了。历史会越来越长工具返回的结果会越来越杂记忆会越攒越多而模型的上下文窗口是有硬上限的。你不主动管理它就会以各种奇怪的方式失效——比如我遇到的 Context 为空。2.2 一个 Agent 的上下文里通常装着什么在 Hello-Agents 的框架里一轮请求发给 LLM 的 Context 大致包含这几类信息系统提示定义 Agent 的身份、能力边界、行为规范通常固定不变对话历史用户和助手之前的往返消息是上下文里增长最快的一块工具调用记录Agent 调用过哪些工具、传了什么参数、拿到什么结果检索到的外部知识从知识库或文档里捞出来的相关片段长期记忆跨会话保留的用户偏好、事实性信息当前轮的用户输入这一轮要处理的新消息这六类信息不是简单堆在一起就完事。它们有优先级、有生命周期、有 token 预算。系统提示一般不动对话历史要按策略裁剪工具结果往往很长需要压缩检索内容要控制条数长期记忆要按相关性筛选。上下文工程的核心工作就是在这几类信息之间做取舍和编排。2.3 为什么第二轮特别容易出问题第一轮请求通常是最简单的系统提示加用户输入结构清晰没什么可丢的。到了第二轮事情就复杂了。你需要把第一轮的用户输入、第一轮的模型回复、可能还有第一轮的工具调用结果全部组装进新的 Context 里。任何一个环节的拼接逻辑写错或者某个字段在传递过程中被覆盖、被清空就会出现我遇到的那种「第二轮 Context 为空」。更隐蔽的是有些框架会把上下文管理拆成多个组件比如一个负责历史裁剪一个负责记忆注入一个负责工具结果格式化。这些组件之间的数据流如果没对齐就会出现「A 组件以为 B 组件会填 ContextB 组件以为 A 组件已经填了」的尴尬局面。终端里看到的只是一个空字段背后可能是一整条数据链路的断裂。3. Hello-Agents 第 9 章的上下文组装设计3.1 整体架构分层组装而不是一次性拼接Hello-Agents 第 9 章的设计思路是分层组装。它没有把所有信息揉成一个大字符串而是把上下文拆成几个独立的层每层有自己的构造逻辑和更新时机最后再按顺序合并成最终发给 LLM 的 messages 数组。这种设计的好处很直接。第一每层可以独立测试你可以在终端里单独打印某一层的内容看它是不是符合预期。第二每层可以独立裁剪比如对话历史太长时只压缩历史层不影响系统提示和工具记录。第三每层可以独立替换比如你想换个记忆存储方案只改记忆层就行不用动其他部分。我实测下来这种分层结构在调试时特别省事。当 Context 为空时我可以逐层打印很快定位到是哪一层没产出内容而不是对着一坨拼接好的字符串猜。3.2 各层的职责与数据流按照第 9 章的思路上下文组装大致分这么几层层级职责更新时机典型 token 占比系统层身份、规范、能力说明初始化时固定5% - 10%记忆层长期偏好、事实每轮按相关性检索5% - 15%知识层检索到的文档片段每轮按查询检索10% - 30%历史层对话往返记录每轮追加并裁剪30% - 50%工具层工具调用与结果工具执行后追加10% - 25%当前层本轮用户输入每轮设置5% - 10%这个占比不是硬性规定而是一个经验参考。实际项目里要根据任务类型调整比如客服类 Agent 历史层占比会更高知识问答类 Agent 知识层占比会更高。数据流是这样的每轮开始时当前层先被设置然后记忆层和知识层根据当前输入去检索历史层从上一轮的状态里继承并追加工具层在工具执行后动态插入最后所有层按固定顺序合并交给 LLM。3.3 为什么顺序不能随便调层的合并顺序是有讲究的。系统层必须放最前面因为它是模型的「行为准则」放后面容易被淹没。当前层通常放最后因为模型对末尾内容的注意力更强把本轮问题放最后能提升响应质量。历史层放中间工具层紧跟在触发它的历史消息后面保持因果连贯。我试过把知识层放到历史层后面结果模型经常忽略检索到的内容因为它被大段对话历史「埋」住了。后来调回历史层之前检索内容的利用率明显上升。这种细节在文档里不一定写但实际跑起来差别很大。4. 核心实现从零组装一轮 Context4.1 环境准备与依赖确认动手之前先把环境理清楚。Hello-Agents 第 9 章的示例代码依赖 Python 3.10 以上主要用到几个库负责调用 LLM 的客户端库、负责 token 计数的工具库、以及框架自带的上下文管理模块。python --version pip list | grep -i agent确认版本没问题后把示例代码拉下来。我建议单独建一个虚拟环境避免和系统里的其他包冲突。python -m venv venv source venv/bin/activate pip install -r requirements.txt提示如果你在 Windows 终端里跑激活命令是venv\Scripts\activate别直接复制 Linux 的命令否则会报找不到文件。4.2 定义上下文层的基类第 9 章的示例里每一层都继承自一个基类基类定义了统一的接口build()负责产出这一层的内容token_count()负责估算 token 数trim()负责在超预算时裁剪。class ContextLayer: def __init__(self, name, priority): self.name name self.priority priority self.content [] def build(self, state): raise NotImplementedError def token_count(self): return sum(len(str(item)) for item in self.content) // 4 def trim(self, budget): while self.token_count() budget and self.content: self.content.pop(0)这里 token 估算用的是「字符数除以 4」的粗略方法中文场景下这个比例不太准实际项目里建议换成正经的 tokenizer。但在调试阶段粗略估算够用了重点是先把数据流跑通。4.3 系统层的构造系统层最简单初始化时设定好就不再变。class SystemLayer(ContextLayer): def __init__(self, system_prompt): super().__init__(system, priority0) self.system_prompt system_prompt def build(self, state): self.content [{role: system, content: self.system_prompt}] return self.content系统提示的写法有讲究。我一般会包含三部分Agent 的身份定位、它能做什么不能做什么、输出格式要求。不要写太长控制在几百字以内太长了反而稀释重点。4.4 历史层的追加与裁剪历史层是上下文里最动态的一块。每轮结束后把本轮的用户输入和模型回复追加进去每轮开始前检查是否超出预算超了就裁剪。class HistoryLayer(ContextLayer): def __init__(self, max_tokens2000): super().__init__(history, priority3) self.max_tokens max_tokens def append(self, role, content): self.content.append({role: role, content: content}) def build(self, state): self.trim(self.max_tokens) return self.content裁剪策略我试过几种。最简单的从头删但会把最早的对话丢掉有时候最早的对话恰恰包含关键背景。后来改成「保留第一条和最近 N 条」中间的直接丢弃效果更稳。再复杂一点可以用摘要把被裁掉的历史压缩成一段摘要保留这个成本高一些但信息损失小。4.5 工具层的动态插入工具层比较特殊它不是每轮都有而是当 Agent 决定调用工具时才产生。工具调用的结果往往很长需要压缩后再放进上下文。class ToolLayer(ContextLayer): def __init__(self, max_result_tokens500): super().__init__(tool, priority4) self.max_result_tokens max_result_tokens def add_call(self, tool_name, args, result): trimmed self._trim_result(result) self.content.append({ role: tool, name: tool_name, content: trimmed }) def _trim_result(self, result): text str(result) if len(text) // 4 self.max_result_tokens: return text[:self.max_result_tokens * 4] ...[truncated] return text工具结果截断是个双刃剑。截太狠模型拿不到关键信息截太少上下文被撑爆。我的经验是结构化结果比如 JSON优先保留字段名和关键值长文本结果优先保留开头和结尾中间截断。4.6 合并与最终请求构造所有层准备好后按优先级顺序合并。def assemble_context(layers, state): messages [] for layer in sorted(layers, keylambda l: l.priority): messages.extend(layer.build(state)) return messages合并完还要做一次全局 token 检查。如果总量还是超了就按优先级从低到高继续裁剪直到符合预算。这一步是最后的安全网防止某一层单独看没超合起来超了。5. 那个「第二轮 Context 为空」的坑我是怎么定位的5.1 现象复现与初步排查回到开头那个问题。第一轮正常第二轮 Context 为空。我先在组装函数里加了一行打印把每一层的内容都输出到终端。for layer in layers: print(f[{layer.name}] {len(layer.content)} items)结果发现历史层是 0 items其他层正常。也就是说第一轮的对话根本没有被追加到历史层里。问题不在组装而在追加环节。5.2 根因状态对象被重新初始化了继续往上查发现每轮开始时会创建一个新的 state 对象而历史层是挂在 state 上的。新 state 一创建历史层就是空的上一轮的内容自然丢了。# 错误写法 def handle_turn(user_input): state AgentState() # 每轮都新建历史丢失 state.history.append(user, user_input) ...正确做法是把 state 提到循环外面或者用一个持久化的会话对象来管理。# 正确写法 state AgentState() while True: user_input input( ) state.history.append(user, user_input) ...这个坑很典型。它不报错不崩溃就是安静地丢数据。如果你没在终端里逐层打印很难发现。5.3 排查这类问题的通用思路我把这类问题的排查思路整理成一张表方便对照。现象可能原因排查方法某层内容为空状态未持久化打印每层 item 数历史越来越短裁剪策略过激检查 trim 阈值模型忽略检索内容层顺序不对调整知识层位置token 超限报错全局检查缺失加合并后总检查工具结果丢失截断逻辑有 bug打印截断前后长度注意排查上下文问题时永远先在终端里逐层打印不要靠猜。上下文是数据流问题肉眼可见的打印比任何推理都可靠。5.4 一个容易被忽略的细节消息角色顺序还有一个坑值得单独说。有些模型对 messages 数组里的角色顺序有隐式要求比如 system 必须在最前user 和 assistant 必须交替。如果你把两条连续的 user 消息拼在一起某些模型会直接报 400 错误提示 context 格式不对。我遇到过一次工具层插入的位置不对导致出现了连续两条 tool 消息模型直接拒绝。后来在合并后加了一个校验函数检查角色序列是否合法问题就再没出现过。def validate_messages(messages): for i in range(1, len(messages)): if messages[i][role] messages[i-1][role]: if messages[i][role] not in (tool,): raise ValueError(f连续相同角色: {messages[i][role]})6. 上下文预算管理token 到底该怎么分6.1 先搞清楚你的模型窗口有多大不同模型的上下文窗口差别很大从几千 token 到上百万 token 都有。动手前先确认你用的模型窗口上限然后留出 20% 左右的余量给输出剩下的才是输入预算。比如窗口是 128k输出预留 4k那输入预算大概 124k。但这不意味着你要把 124k 塞满实际用下来塞得越满模型对中间内容的注意力越差响应质量反而下降。我的经验是把输入控制在窗口的 50% 到 70% 之间效果最稳。6.2 各层的预算分配策略预算分配没有标准答案但有个原则越靠后、越和当前问题相关的层预算越充足。系统层固定通常几百 token记忆层按相关性取 top 3 到 top 5控制在 1k 以内知识层按相关性取 top 3 到 top 5 片段控制在 2k 到 4k历史层占大头可以给到总预算的 40% 到 50%工具层按需单个结果控制在 500 token 以内当前层用户输入本身通常不大这个分配在客服、问答、任务执行三类场景里都试过基本够用。特殊场景再微调。6.3 动态调整而不是一刀切固定预算在简单场景够用但复杂场景下会出问题。比如某一轮用户问了一个需要大量检索的问题知识层需要更多预算这时候就该从历史层借一点。我实现过一个简单的动态分配先给每层一个基础预算然后根据当前输入的类型调整。如果检测到是知识型问题知识层预算翻倍历史层减半如果是闲聊型历史层保持知识层压缩。def allocate_budget(query_type, total_budget): if query_type knowledge: return {history: total_budget * 0.3, knowledge: total_budget * 0.5} elif query_type chat: return {history: total_budget * 0.6, knowledge: total_budget * 0.1} else: return {history: total_budget * 0.45, knowledge: total_budget * 0.3}这个逻辑不复杂但效果提升明显。模型在知识型问题上的回答准确率上去了闲聊时的连贯性也没丢。7. 实操心得与常见问题速查7.1 我踩过的几个典型坑第一个坑就是前面说的状态未持久化。这个坑的教训是上下文相关的对象生命周期要和会话对齐不要和单轮对齐。第二个坑是工具结果没截断。有一次调用了一个返回大 JSON 的工具结果直接把上下文撑爆模型报 400。后来加了截断但截断位置没选好把关键字段截掉了模型拿到的是一堆无意义的开头。最后改成保留字段名加前几个值问题才解决。第三个坑是记忆层检索太宽泛。早期我用向量检索取 top 10结果塞进去一堆不相关的内容模型反而被干扰。后来收紧到 top 3并且加了一个相关性阈值低于阈值的直接不要效果好了很多。7.2 常见问题速查表问题原因解决第二轮 Context 为空状态对象每轮重建会话级持久化 state模型报 400 超长未做全局 token 检查合并后统一裁剪模型忽略检索内容知识层位置靠后调整到历史层之前工具结果丢失截断逻辑截错位置保留字段名和关键值历史裁剪丢关键信息从头删策略保留首条加最近 N 条角色顺序报错连续相同角色合并后校验角色序列7.3 几个提升稳定性的小技巧第一给每一层加日志。不是打印全部内容而是打印层名、item 数、token 估算值。这样出问题时一眼能看出是哪层异常。第二写一个上下文快照功能。每轮组装完把最终的 messages 存一份到本地文件出问题时可以回放。这个在调试复杂多轮场景时特别有用。第三给裁剪逻辑写单元测试。构造一个超长的历史跑一遍裁剪断言结果符合预期。裁剪逻辑是最容易出 bug 的地方有测试兜底心里踏实。第四token 估算别用字符数除以 4。中文一个字大概 1 到 2 个 token英文一个词大概 1 到 1.5 个 token。用正经的 tokenizer 算误差小很多。调试阶段可以粗略上线前一定要换准的。8. 上下文工程后续还能怎么扩展跑通第 9 章的基础版本后我顺着往下做了几个扩展效果都不错分享给你参考。第一个扩展是上下文压缩。当历史层超预算时不是简单丢弃而是调用一次 LLM 把旧历史压缩成摘要。这样信息损失小但多了一次模型调用成本和延迟都上去了。适合对连贯性要求高的场景。第二个扩展是分层记忆。把长期记忆再拆成「事实记忆」和「偏好记忆」事实记忆用向量检索偏好记忆直接全量注入。偏好通常不多全量注入反而更稳。第三个扩展是上下文缓存。系统层和记忆层在很多轮里是不变的可以把这部分缓存起来只重新计算变化的部分。这个在长会话里能省不少 token 和时间。第四个扩展是上下文可视化。写一个简单的终端界面把每一层的内容用不同颜色打印出来直观看到上下文是怎么构成的。调试时特别爽一眼就能看出哪层占了大头。这些扩展不是必须的但如果你打算把 Agent 用到生产环境迟早会碰到需要它们的时候。先把基础版本跑稳再按需加别一上来就全上那样调试成本太高。我个人在实际操作中的体会是上下文工程最难的从来不是写代码而是想清楚「这一轮模型到底需要看到什么」。代码只是把这个思考落地思考不清楚代码写得再漂亮也是白搭。每次出问题先回到这个问题上问自己一遍往往比盯着代码看半天更有效。
企业数字化 ERP 产品动态
相关推荐
JSON转Sketch插件实战:用json-sketchapp自动生成设计稿 简介:json-sketchapp 是一款面向设计师与前端开发者的 Sketch 插件,用于将 JSON 数据文件一键转换为 Sketch 设计稿,适合需要从数据驱动批量生成界面、搭建设计规范或探索自动化设计流程的团队。插件基于 skpm 构建,并借助 Sketch… · 2026/9/26 21:57:12
Parallels Desktop macOS闪退修复:codesign重签名实战指南 1. 项目概述:这不是崩溃,是签名失效的“温柔驱逐”Parallels Desktop 在 macOS 上突然退出、反复闪退、启动后几秒就消失——这种问题我过去三年里处理过至少47次,覆盖 macOS Monterey 到 Sonoma 全版本,涉及 Parallels Desktop 1… · 2026/9/26 21:56:58
2026版GPT-5.5迭代解析:百万上下文与Agent编程质变,TaoToken统一Key接入配置实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 22:24:15
3步搞定wordpress引用js,新手入门避坑指南 3步搞定wordpress引用js,新手入门避坑指南 改个需求建站公司拖一周,这种憋屈谁懂?上个月客户急着上线促销页,让我在WordPress后台加个倒计时JS,报价三千块工期五天。我直接翻了白眼,这活儿我自己十分钟就能干完。其实对于想自己… · 2026/9/26 22:24:15
织梦网站栏目设计避坑指南:懂代码才能知道多少钱 织梦网站栏目设计避坑指南:懂代码才能知道多少钱 找建站公司怕被坑高价,问一句“做个织梦站栏目怎么设计”,对方张嘴就是八千、一万,连个报价单都拿不出来。这种黑箱操作,谁心里不犯嘀咕?其实,织梦(DedeCMS)的栏目设计成本,很大程度上取决于… · 2026/9/26 22:24:09
市盈率、市净率还是DCF?《投资入门指南》新手必会的股票估值完整指南 市盈率、市净率还是DCF?《投资入门指南》新手必会的股票估值完整指南 【免费下载链接】investing-for-beginners 美股、期权与加密货币知识框架 项目地址: https://gitcode.com/gh_mirrors/in/investing-for-beginners
股票估值是买入任何股票前最重要的一步… · 2026/9/26 22:23:56
吉林市网站创意与建设选哪家,源码下载别踩坑 吉林市网站创意与建设选哪家,源码下载别踩坑 改个需求建站公司拖一周?别忍了,直接把源码下载到自己手里。在吉林市做网站创意与建设,很多老板觉得“外包省事”,结果发现对方不仅响应慢,还卡着核心技术不放。一旦想换服务商或者自己微调,对方要么加价,… · 2026/9/26 22:23:56
wordpress显示缩略图摘要怎么选不踩坑3个实战案例 wordpress显示缩略图摘要怎么选不踩坑3个实战案例 自己不会代码想做网站,是不是每次看到那种“左边一张精美缩略图,右边几行摘要文字”的列表页,心里既痒又慌?怕改乱了样式,怕代码报错,更怕花了钱请人做,结果对方收你高价还做得慢。其实,… · 2026/9/26 22:23:49
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46