context-mode实战:为终端问答助手构建四层上下文管理机制 📅 发布时间:2026/9/11 12:25:47 👁 浏览次数: 聊一个最近在多个项目里反复踩坑又反复受益的话题context-mode。简单说它就是一种“带着上下文去处理请求”的模式。如果你在做 AI 应用、命令行工具、自动化脚本或者任何涉及多轮交互的软件你迟早会遇到这个问题单看某一句话时程序不知道你在说什么但如果把前因后果一起给它它瞬间就明白了。context-mode 就是把这个“前因后果”显式地建模、存储、组装再喂给模型或逻辑处理器。这篇内容我结合自己做一个终端问答助手的经历把 context-mode 从概念到代码、从坑点到排查思路完整拆一遍希望对正在做类似东西的人有帮助。1. context-mode 是什么以及为什么现在才火1.1 从无状态命令说起传统命令行工具大多是无状态的。你输入ls它列出当前目录你输入grep foo bar.txt它去文件里搜 foo。每一条命令都是独立的不需要记住上一条命令干了什么。这种设计简单、可靠也是 Unix 哲学的一部分。但在 AI 时代无状态变成了一个很别扭的限制——因为用户的问题往往是围绕一个目标连续发生的。比如你在终端里写你帮我看看 src/ 下哪些文件改动最大 助手根据 git log 分析utils.py 改动最多共 23 次 你那它主要改了什么功能第二句话“那它”指的是哪个文件如果你不给模型上下文它只能瞎猜。如果你把第一轮的分析结果、提到的文件名、甚至相关 diff 都一起传过去模型就能准确回答“utils.py 主要修改了配置解析相关的功能”。这就是 context-mode 的核心价值让程序“记得住”刚才的事。1.2 context-mode 的三个核心价值第一个价值是降低歧义。“它”“那个”“上面提到的”这类指代词只有结合上下文才能消解。第二个价值是连续性用户不需要每次把完整背景重复一遍工具可以从之前的对话里自动提取。第三个价值是决策质量模型在获得了更完整的信息后给出的答案往往更精准而不是基于残缺信息的泛泛而谈。我举个例子。早期我做一个内部文档问答工具用户经常问“这个接口怎么调用”。如果不带上下文模型只能给一个通用模板如果带上用户当前所在的项目、已经阅读过的代码片段、以及之前问过的接口版本模型就能给出针对当前代码库的具体用法。同样是“怎么调用”答案质量差了一个数量级。1.3 适用场景与不适合的场景context-mode 并不是万能的。我总结了一下自己用下来最合适的几类场景多轮对话助手客服机器人、编程助手、知识库问答这些天然需要记忆。代码生成与补全IDE 插件类的工具需要感知光标位置、当前文件内容、项目结构。自动化任务编排批处理脚本里后一步的结果依赖前一步的输出。测试与调试工具需要追踪一个操作序列而不是单次请求。不适合的场景也有。比如高频、低延迟的简单查询像“这个目录多大”“当前时间是多少”强行加上下文反而增加开销。还有就是数据敏感性极高的场景如果上下文里包含了用户不想被记录的信息那就要慎重设计存储和清理策略。我的原则是上下文能少则少只有当丢掉了会出问题时才加上。2. 结构先行把上下文拆成四层2.1 第一层用户当场说的话这一层最好理解就是用户当前这一次输入的内容。它是一切处理的起点。很多人会忽略的是这一层也需要稍微做点加工而不是原文直接丢进去。比如用户的输入可能包含错别字、口语化表达、或者隐晦的指代需要在上层组装时做归一化处理。我在终端助手项目里会把用户原始输入保留一份用于存档同时生成一份“规范化版本”用于后续组装。规范化版本会修正明显的笔误把“帮我搞一下”这种模糊表述补充成“请对当前文件执行重构建议”。这一步对最终效果影响很大因为模型在越清晰的指令下表现越好。2.2 第二层之前的对话和动作这一层是 context-mode 的核心也是最容易做坏的部分。它至少应该包含历史消息用户和助手之间过往的每一轮问答。执行动作助手或系统在这期间调用过哪些函数、读取过哪些文件。中间结果比如运行过某个命令后产生的输出或者读取到的文件内容片段。存储形式我建议用消息列表而不是一段拼接好的字符串。因为消息列表可以精确控制哪些轮次保留、哪些可以丢弃也可以动态调整优先级。每个消息至少要记录三个字段角色、内容、时间戳。角色区分 user、assistant、system、tool时间戳用于后续做“过期清理”。这里有个要点不是所有历史都要无脑带上。很多新手会把十轮对话全部塞进 prompt结果 token 爆炸模型反而被无关信息干扰。我后面会专门讲裁剪策略。2.3 第三层环境信息与业务数据这一层很多人会忽视但它往往是让模型“开口说人话”的关键。环境信息包括当前时间、当前操作系统、当前目录、当前打开的文件、用户身份等。业务数据包括项目相关的配置、数据库里的记录、监控指标等等。举一个实际例子。我的终端助手工具里内置了一个“当前环境”模块每次组装上下文时自动附上当前时间2025-06-14 21:30:22 工作目录/home/dev/myproject Git 分支feature/context-mode 最近提交fix: 调整上下文压缩阈值就这么几行信息模型在回答时就不会再问“你的项目在哪个目录”“你现在在哪个分支”之类的蠢问题。因为这些基础信息已经在上下文里了它可以直接基于真实环境作答。如果你的工具面向的是一个垂直领域比如客服系统那么环境信息可能变成“当前工单编号”“客户等级”“最近三笔订单金额”效果也是一样的。2.4 第四层系统规则与约束最后一层是给模型定规矩的“系统指令”。它描述了助手的人设、回答风格、限制条件、输出格式。在 context-mode 的语境下这一层起到了“总的宪法”的作用前面三层是“具体事实”这一层是“处理方式”。举个例子我在文档问答工具里设置的系统指令是你是一个严谨的软件工程师。回答必须基于给定的上下文遇到超出范围的问题请明确说你不知道。输出格式优先使用 Markdown。不要编造函数名和路径如果上下文里没有请如实说明。系统指令和上下文的区分很重要。如果不区分模型很容易把“规则”和“事实”混在一起处理导致时而遵守规则、时而忘记规则。当你把它们明确分层后模型的稳定性和可控性都会有明显提升。2.5 一个组装后的上下文实例理论讲再多不如看一条组装后的实际 prompt。下面是我终端助手项目里真实跑过的一条上下文简化版[system] 你是一名严谨的软件工程师擅长代码分析和重构建议。 回答基于给定的上下文不得编造不存在的文件或函数。 使用简洁的中文回答必要时给出代码示例。 [context] 当前时间2025-06-14 21:30:22 工作目录/home/dev/myproject Git 分支feature/context-mode 最近提交fix: 调整上下文压缩阈值 [conversation] user: 帮我看看代码里哪里用到了 config 模块 assistant: 在 config.py 中有 ConfigManager 类主要被 main.py 和 db.py 引用。 user: 那 db.py 里的连接串是从 ConfigManager 取的吗看到没有第三轮的问题“那 db.py 里的连接串”因为有了前两轮的历史模型知道这里的“那”指的是“config 模块”也知道 db.py 是刚才提到的那个文件。如果去掉历史这个问题几乎没法回答。3. 实操落地给 CLI 工具加上 context-modePython 版3.1 整体架构与目录设计讲完了理论直接上实操。我以自己做过的一个“终端上下文问答”小工具为例语言选 PythonLLM 调用以 OpenAI 兼容接口为例。工具的核心功能是在终端里连续提问每次提问都能带上之前的对话、当前目录、Git 状态等信息。先看目录结构非常简单context_mode/ ├── __init__.py ├── cli.py # 入口参数解析 ├── session.py # 会话管理负责存储和读取历史 ├── assembler.py # 上下文组装把四层信息拼成消息列表 ├── budget.py # token 预算计算与裁剪 └── llm.py # 调用大模型接口每个文件承担一个职责后面加功能会非常方便。你不要一上来就搞太重型的框架CLI 工具的核心逻辑就是读输入、组装上下文、调用模型、输出结果、保存历史。3.2 核心代码会话存储与消息组装先看会话管理部分。它的任务是从磁盘加载历史消息并允许追加新消息。我直接用 JSON Lines 格式存储每行一条消息简单可靠import json import os SESSION_DIR os.path.expanduser(~/.context_mode_sessions) def _session_path(session_id: str) - str: return os.path.join(SESSION_DIR, f{session_id}.jsonl) def load_messages(session_id: str): path _session_path(session_id) if not os.path.exists(path): return [] messages [] with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if line: messages.append(json.loads(line)) return messages def append_message(session_id: str, role: str, content: str): path _session_path(session_id) os.makedirs(SESSION_DIR, exist_okTrue) msg {role: role, content: content, ts: time.time()} with open(path, a, encodingutf-8) as f: f.write(json.dumps(msg, ensure_asciiFalse) \n) return msg这里存储的不是字符串而是结构化对象后面做裁剪、排序、统计都方便。时间戳字段很关键当你需要清理长时间未活动的会话时直接按 ts 排序删除即可。再看上下文组装这是 context-mode 最核心的部分。它的工作就是把四层信息合并成一个标准的消息数组供模型接口调用import datetime import subprocess def get_environment_info(cwd: str) - str: info [f当前时间{datetime.datetime.now()}] info.append(f工作目录{cwd}) try: branch subprocess.check_output( [git, -C, cwd, rev-parse, --abbrev-ref, HEAD], stderrsubprocess.DEVNULL, textTrue, ).strip() info.append(fGit 分支{branch}) except Exception: pass return \n.join(info) def build_messages(session_id: str, user_input: str, cwd: str): system_prompt ( 你是一名严谨的软件工程师擅长代码分析和重构建议。\n 回答基于给定的上下文不得编造不存在的文件或函数。\n 使用简洁的中文回答必要时给出代码示例。 ) env_info get_environment_info(cwd) history load_messages(session_id) messages [{role: system, content: system_prompt}] messages.append({role: system, content: f[context]\n{env_info}}) messages.extend(history) messages.append({role: user, content: user_input}) return messages我习惯把环境信息也作为 system 角色插入。这样模型会把它当作“背景知识”而不是“用户指令”不会和用户的真实语义混淆。注意历史消息的顺序越早的越靠前最新的一条 user 输入放在最后这是大多数 LLM 接口默认的聊天格式。3.3 做个“上下文预算”函数上下文不是无限塞的每一家大模型都有窗口限制。比如你的模型上下文窗口是 8k token那你的 prompt 不能超过 8k还必须预留一部分给模型输出。如果不控制请求直接报错或者被静默截断回答质量急剧下降。我写了一个简单的预算控制逻辑思路很朴素从系统指令开始加上环境信息然后倒序加入历史直到总 token 数接近预算阀值。之所以倒序是因为越靠近当前时间的内容通常越重要。def estimate_tokens(text: str) - int: # 粗略估计中英文混合场景下比较保守 return int(len(text) * 1.3) def fit_messages_within_budget(messages, max_input_tokens6000): fitted [] total 0 # 开头固定保留 system 消息前两条 for msg in messages[:2]: fitted.append(msg) total estimate_tokens(msg[content]) # 历史部分倒序选择 history messages[2:-1] tail messages[-1] for msg in reversed(history): size estimate_tokens(msg[content]) if total size max_input_tokens: break fitted.insert(-1, msg) total size fitted.append(tail) return fitted注意我这里用了一个很粗的 token 估算函数真实场景建议接入 tiktoken 或者对应模型的 tokenizer。估算公式 len(text) * 1.3 是我在实践中调出来的经验值对中文比较友好。如果你处理的英文内容多乘 1.1 左右就够。这个值偏差不致命因为你真正的目的是“预防溢出”而不是“精确计数”。3.4 运行效果与配置参数说明最后把入口组装起来。cli.py 接受一个 session_id 参数用于区分不同场景的上下文。比如你可以在项目 A 里用 session 名 “project-a”项目 B 里用 “project-b”历史互相独立import argparse from session import append_message, load_messages from assembler import build_messages, get_environment_info from budget import fit_messages_within_budget from llm import chat_completion def main(): parser argparse.ArgumentParser(descriptioncontext-mode 终端助手) parser.add_argument(--session, defaultdefault) parser.add_argument(--cwd, default.) parser.add_argument(--max-tokens, typeint, default6000) args parser.parse_args() user_input input(你) messages build_messages(args.session, user_input, args.cwd) messages fit_messages_within_budget(messages, args.max_tokens) reply chat_completion(messages) print(助手, reply) append_message(args.session, user, user_input) append_message(args.session, assistant, reply) if __name__ __main__: main()--max-tokens这里表示输入侧最多允许多少 token你还需要在调用模型时单独设置输出的 token 上限。如果模型总窗口是 8k输入占 6k那输出就只剩 2k超出也可能报错。我通常会把输入预算定为总窗口的 70% 到 75%留出足够的生成空间。模型调用部分没什么特别的就是标准的 chat completion 接口把你的 messages 数组传过去即可。我在这里就不贴完整调用了重点是通过上面这段代码大家能看到 context-mode 的完整闭环读取用户输入、组装四层上下文、做预算控制、调用模型、把结果存入会话。4. 常见问题与排查技巧实录4.1 上下文污染历史里的错误答案被带进新问题这是 context-mode 踩坑概率最高的问题。上下文带上了模型确实能记住之前的话但之前如果回答错了错误的信息也会被当作“事实”继续使用甚至越传越离谱。我遇到过一种典型情况第一轮模型误以为某个配置文件位于根目录第二轮用户问“基于刚才的分析帮我写个清理脚本”模型就在脚本里写了对一个不存在路径的操作。排查时把历史的 assistant 消息拉出来一看源头就是第一轮那个错误路径。我的对策是两招。第一在系统指令里明确写“如果历史信息与当前现实环境存在冲突以当前环境信息为准”。这样模型在组装答案时就不会无条件信任历史。第二做关键信息校验。如果历史里提到文件路径、函数名、依赖版本组装上下文时抽出来和当前项目真实情况做一次比对不一致的直接追加一条 system 提示说明冲突让模型自行修正。4.2 上下文溢出飘红报错输出质量断崖式下跌模型有窗口限制上下文塞太多就直接报错。但比报错更隐蔽的问题是“接近上限但没超”。这时候模型可能被迫截断你给它的内容或者生成到一半没空间了结果输出断在一半。我的排查经验是不要只盯着报错信息要把实际发送给模型的 prompt 完整打印出来看一眼 token 消耗。我习惯在调试模式下输出如下信息[debug] input tokens: 5832, budget: 6000, output tokens: 1024如果发现 input tokens 长期高位运行说明你的裁剪策略不够激进。此时有几个方案加大裁剪阈值引入摘要机制把更早的历史用一小段 summary 替代或者直接限制历史最大轮数。我最后选择了“摘要 最近 N 轮”组合超过某个时间点的消息不再原文保留而是先让模型生成一段压缩摘要再带着摘要继续。这样虽然损失了部分细节但上下文锚点不会完全丢失。4.3 上下文带了但模型好像根本没用上比溢出更让人崩溃的是上下文信息明明都放进去了模型还是回答得像个失忆的人。我自己复盘过很多次这类问题通常有三个原因。第一个原因是信息摆放位置不对。模型对 prompt 开头和结尾的内容敏感度更高中间部分容易被“淹没”。如果你有一批重要上下文尽量把最关键的放开头或结尾。第二个原因是格式不够机器可读。你给模型一段很长很乱的环境变量日志它很难提取关键信息但如果整理成清晰的分点一行一个事实效果立竿见影。第三个原因是提示词里没有“如何使用这些上下文”的指引。你光给了上下文没告诉模型“回答时优先基于这些信息”它就会倾向于依赖自己的预训练知识。针对这三个原因我的做法是重要信息放在 system 指令之后紧挨着的显眼位置所有环境信息都整理成短行形式系统指令里明确写死“必须优先使用 标签内的信息作为事实依据”。这三个改动加完效果提升非常明显。4.4 性能与成本问题上下文越多速度越慢、账单越高每轮请求都携带全部历史token 消耗是线性的会话越长消耗越大。如果你的工具是个人用还好一旦接入线上服务账单会非常难控制。我见过一个 Demo 项目在压力测试时因为上下文无限增长单轮 prompt 冲到 3 万 token接口延迟飙到 20 秒以上成本直接吓人。常规解法是给会话设置最大轮数超过之后自动触发摘要压缩。还有一个更聪明的做法是“按需带回”不是每轮都带全部历史而是根据当轮输入判断需要哪些前文。这种方案实现成本高一点但效果很好。我目前采用的是折中方案最近 10 轮完整保留10 轮之前压缩成两三条摘要再早的直接清掉。这样对话的连贯性保住了token 消耗也被控制在可接受范围。4.5 排查工具与方法把上下文打回原型调试 context-mode 最难的一点是模型输出的问题往往不是模型本身的问题而是“你给它的信息出了问题”。所以我养成了一个习惯不看模型输出先看输入。每次请求之前把组装好的 messages 整个序列化打印出来逐条检查有没有重复信息有没有过期信息有没有冲突信息有没有缺失关键信息我在项目里加了一个--debug参数开启后会把最终发送给模型的 prompt 保存到本地文件。当用户反馈“回答不准”我可以直接把他当时的 prompt 文件拿出来逐条对照分析。这个过程其实就是 context-mode 的“黑盒复现”非常有效。再补充一个技巧用极简用例做 A/B 测试对比到底哪部分上下文起了作用。比如同一句提问分别用“不带环境信息”“只带历史”“只带环境信息”“全量上下文”四种组合去测每组的回答差异一目了然。有个项目通过这个测试发现加了 Git 分支信息之后模型对“当前改动”类问题的回答准确率提升了近三成我才意识到环境信息对代码场景的重要性远比想象中大。写在最后我个人在实际项目里的体会是context-mode 本质上是一种信息组织能力不是简单的“把历史加进去”。它考验的是你能不能判断哪些信息真正影响最终结果并以清晰、非冗余的方式呈现。做这个终端工具的过程里我反复调整了四层结构的优先级和裁剪策略每改一次模型的稳定性就上一步。你如果在做类似的东西建议从小处着手先只保留最近的几轮对话跑通再从环境信息和系统规则逐步加千万不要一上来就追求“全都要”。上下文这个东西真正值钱的是决策相关的那部分其余的只是在污染模型的视野。