news 2026/9/11 12:25:39

context-mode实战:为终端问答助手构建四层上下文管理机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
context-mode实战:为终端问答助手构建四层上下文管理机制

聊一个最近在多个项目里反复踩坑又反复受益的话题: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-mode(Python 版)

3.1 整体架构与目录设计

讲完了理论,直接上实操。我以自己做过的一个“终端上下文问答”小工具为例,语言选 Python,LLM 调用以 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", encoding="utf-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_ok=True) msg = {"role": role, "content": content, "ts": time.time()} with open(path, "a", encoding="utf-8") as f: f.write(json.dumps(msg, ensure_ascii=False) + "\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"], stderr=subprocess.DEVNULL, text=True, ).strip() info.append(f"Git 分支:{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_tokens=6000): 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(description="context-mode 终端助手") parser.add_argument("--session", default="default") parser.add_argument("--cwd", default=".") parser.add_argument("--max-tokens", type=int, default=6000) 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 本质上是一种信息组织能力,不是简单的“把历史加进去”。它考验的是你能不能判断哪些信息真正影响最终结果,并以清晰、非冗余的方式呈现。做这个终端工具的过程里,我反复调整了四层结构的优先级和裁剪策略,每改一次,模型的稳定性就上一步。你如果在做类似的东西,建议从小处着手:先只保留最近的几轮对话,跑通再从环境信息和系统规则逐步加,千万不要一上来就追求“全都要”。上下文这个东西,真正值钱的是决策相关的那部分,其余的只是在污染模型的视野。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 12:23:58

专科生AI时代工具选择指南:8款实测推荐

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:17:05

Python迭代器深度解析:从for循环原理到生成器与itertools实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:17:04

OpenMontage 技能库实战:基于 Tailwind CSS v4 构建可扩展设计系统

OpenMontage 技能库实战:基于 Tailwind CSS v4 构建可扩展设计系统 【免费下载链接】OpenMontage Worlds first open-source, agentic video production system. 12 production pipelines, 100 tools, 700 agent skill and production-knowledge files. Turn your A…

作者头像 李华