Codex++本地部署中自动压缩上下文的工程实践

Codex++本地部署中自动压缩上下文的工程实践

1. 项目概述:为什么“自动压缩上下文”是 Codex++ 接入 llama.cpp 后绕不开的硬骨头

Codex++ 不是玩具,它是个正经跑在本地、靠 llama.cpp 做推理引擎的代码智能助手。我第一次把 Codex++ 指向一个 2000 行的 Python 工程目录,让它“帮我重写这个数据清洗模块”,结果等了三分钟,浏览器只甩给我一个冷冰冰的 502 Bad Gateway。不是模型卡死,不是显存爆了,而是请求压根没进到 llama.cpp 的推理循环里——它被拦在了 HTTP 层。查日志发现,Codex++ 把整个工程的文件树、所有依赖的 README、甚至 .gitignore 里的路径都塞进了 system prompt 和 user message,拼出来的 JSON payload 超过了 128KB,而默认的 nginx 或 Windows 自带的 http.sys 代理层对单次 POST 请求体有严格限制。这根本不是模型能力问题,是上下文管理失控导致的链路断裂。

“自动压缩上下文”这个词听起来像 AI 黑话,但落到 Codex++ + llama.cpp 这个组合上,它就是一条生死线。Codex++ 的核心价值在于理解你当前编辑器里的真实代码上下文(当前文件、光标位置、选中代码块、关联的头文件或 import 链),而不是把整个项目目录打包扔给模型。llama.cpp 的优势是轻量、可控、离线,但它没有 OpenAI 那套成熟的上下文路由和动态截断机制。你不能指望它自己判断“这段 README 对当前函数重写没用,可以砍掉”。所以,这个“自动压缩”,不是简单地按 token 数硬切,而是要模拟一个资深开发者在看代码时的注意力分配:哪些是必须保留的骨架(函数签名、类型注解、关键变量名),哪些是可折叠的血肉(大段日志打印、冗长的注释、已废弃的 TODO),哪些是完全无关的噪音(.DS_Store、node_modules 路径、编译中间文件)。我做的这个压缩模块,本质上是一个运行在 Codex++ 后端的、基于语义规则的上下文预处理器,它在请求发往 llama.cpp 之前,就把原始输入“翻译”成模型真正能消化、且不丢关键信息的精简版。它不碰模型权重,不改 llama.cpp 源码,只在 Codex++ 的 API 网关层加了一道智能过滤阀。适合谁?适合所有在 Windows 11 上配 CUDA 版 llama.cpp、用 Codex++ 做本地开发辅助,却总被 502、超时、响应慢折磨得想砸键盘的人。它解决的不是“能不能用”,而是“能不能稳、能不能快、能不能准”。

2. 核心设计思路与方案选型:为什么不用 llama.cpp 内置的 context window 截断?

2.1 为什么不能依赖 llama.cpp 的 -c 参数或 --ctx-size?

这是最常被问到的问题。很多人以为,只要把 llama.cpp 启动时的 context size 设得足够大(比如 -c 32768),问题就一劳永逸了。错。原因有三,而且每一条都直击痛点:

第一,内存与显存的硬性天花板。llama.cpp 的 context size 不是免费的午餐。每个 token 在 KV Cache 中占用的显存/内存,大致是2 * n_layers * n_kv_heads * head_dim * sizeof(float)。以 Qwen2-7B-Instruct 为例,n_layers=28,n_kv_heads=4,head_dim=128,float 是 4 字节。算下来,一个 token 就要吃掉约 2 * 28 * 4 * 128 * 4 ≈ 229 KB 的显存。32K context 就是 229 KB * 32768 ≈ 7.5 GB!这还没算模型权重本身。你在 RTX 4060 上跑,显存直接见底,启动都报错。而 Codex++ 的典型请求,往往只需要 2K~4K tokens 的有效上下文,强行喂满 32K,是用 10 倍的资源换 1% 的边际收益,纯属浪费。

第二,语义完整性被暴力破坏。llama.cpp 的截断是“从后往前硬砍”。假设你传入的是:“[system] 你是一个 Python 专家... [user] 请分析以下函数:def process_data(df: pd.DataFrame) -> dict: ... (100 行代码)... # TODO: add error handling for empty df”。如果 context 不够,llama.cpp 会把最后的# TODO: add error handling...这行关键提示直接砍掉,模型看到的是一段戛然而止的代码,它怎么知道你要它“加错误处理”?它只能瞎猜。这种截断,等于让一个医生只看病人 CT 片的下半部分,然后让他诊断全身疾病。

第三,无法区分“重要上下文”与“干扰上下文”。Codex++ 的请求体里,除了用户当前编辑的代码,还混着大量元信息:VS Code 的 workspace path、当前打开的 tab 列表、Git 分支名、甚至 Codex++ 插件自己的 debug 日志。这些信息对模型推理毫无价值,但它们占满了宝贵的 token 预算。llama.cpp 的 -c 参数对它们一视同仁,全都要塞进去。这就像去图书馆借书,管理员不问你要哪本,直接把整层楼的书都给你扛走,美其名曰“给你最大的选择空间”。

所以,我的方案是“上游压缩,下游轻载”。在 Codex++ 收到编辑器发来的原始请求后,立刻启动一个独立的、轻量级的预处理流水线。它不修改 llama.cpp,也不增加任何 GPU 计算负担,只用 CPU 做几毫秒的文本分析和重构。目标很明确:把 128KB 的原始请求,精准压缩到 4KB 以内,且保证所有对代码理解至关重要的信号(函数名、参数、返回值、关键注释、错误提示)一个不丢。这比在 GPU 上硬扛一个超大 context,要高效、稳定、经济得多。

2.2 为什么选 Codex++ 的后端做压缩,而不是前端(VS Code 插件)或 llama.cpp 的自定义 backend?

技术上,三个位置都能做,但权衡之后,后端是唯一合理的选择。

  • 前端(VS Code 插件)做压缩?不行。VS Code 插件运行在 Node.js 环境,它的文本处理能力有限,尤其面对大型文件或复杂 AST 解析时,容易卡住 UI 线程,导致编辑器假死。更重要的是,插件无法访问完整的上下文源。它知道当前文件,但不知道import utils导入的utils.py文件内容,也不知道from config import settingsconfig.py的结构。这些跨文件的依赖关系,只有 Codex++ 后端在解析 workspace 时才能完整获取。把压缩逻辑放在前端,等于让一个近视眼在黑暗里摸象,只能看到鼻子,看不到全貌。

  • llama.cpp 的自定义 backend(如 custom backend)做?更不行。llama.cpp 的 backend 是为模型推理服务的,它的设计哲学是“极简、专注、零依赖”。你往里面塞一个复杂的上下文分析器,会严重污染它的代码库,增加维护成本,也违背了 llama.cpp 社区“只做推理”的共识。而且,backend 层的错误日志非常难调试,一旦出问题,你分不清是模型崩了还是你的压缩逻辑错了。把它当成一个黑盒推理引擎来用,是最稳妥的。

  • Codex++ 后端做?天然是最佳位置。Codex++ 本身就是整个工作流的“大脑”。它已经建立了与 VS Code 的 LSP(Language Server Protocol)连接,能实时获取光标位置、选中范围、符号定义跳转(Go to Definition)的结果。它已经集成了文件系统 watcher,能监听 workspace 的变化。它还内置了简单的 AST 解析器(用于高亮和跳转),这意味着它对代码的语法结构有基本认知。把压缩模块嵌入 Codex++ 的 API handler(比如/v1/chat/completions的入口函数),相当于在数据流的咽喉处装了一个智能阀门。所有进入的请求,先过这个阀,再流向 llama.cpp。它能拿到最全的信息,付出最小的性能代价,也最容易调试和迭代。我试过把压缩逻辑写成一个独立的 microservice,结果发现网络延迟和序列化开销反而抵消了压缩带来的收益。直接集成进 Codex++ 进程,是零拷贝、零延迟的最优解。

2.3 压缩策略的核心思想:从“token 计数”到“语义保真度”

市面上很多“上下文压缩”工具,本质就是个高级版text[:max_tokens]。我的方案完全不同,它的核心思想是“语义保真度优先”。我不关心最终 token 数是多少,我只关心:模型看到这个压缩后的文本,是否能和看到原始文本一样,准确无误地完成你交代的任务?为此,我设计了三层过滤网:

  1. 结构层(Structure Filter):识别并保留所有构成代码“骨架”的元素。这包括:函数/类/方法的定义行(def foo(,class Bar:)、参数列表(含类型注解)、return 类型声明、if/for/while的条件表达式、try/except的异常类型。这些是模型理解“程序在做什么”的绝对基石。任何压缩,都不能让def calculate_total(items: List[Item]) -> float:变成def calculate_total(...),因为类型信息丢失,模型就可能返回一个字符串而不是浮点数。

  2. 意图层(Intent Filter):识别并强化所有表达用户“当前意图”的信号。这主要来自两部分:一是 Codex++ 插件在发送请求时,会在 system prompt 里注入的 context-aware 指令,比如 “你正在编辑src/utils/data.py,光标位于第 42 行,用户选中了def clean_text()函数体”。二是用户在 chat 输入框里写的自然语言指令,比如 “把这个函数改成支持异步”。这些指令是任务的“灵魂”,必须原样保留,甚至加权提升其在 prompt 中的权重(比如在它前后加--- INTENT START ---/--- INTENT END ---的标记)。

  3. 噪声层(Noise Filter):这是最体现功力的部分。它不是简单地删注释或空行,而是基于规则和启发式,判断一段文本是否真的“无关”。例如:

    • # TODO: fix this later这种注释,如果它出现在当前选中的函数内部,就是强相关信号,必须保留;如果它出现在一个完全不相关的配置文件里,就可以安全删除。
    • 大段的print("DEBUG: ...")日志,如果DEBUG字样出现在当前函数名或变量名里(比如debug_mode = True),那它可能是关键线索,要保留;否则,一律归为噪声。
    • .gitignore文件内容,对模型理解当前代码逻辑毫无帮助,直接丢弃。

这三层过滤,共同构成了一个“有思考”的压缩器。它不追求极致的压缩率,而是追求最高的“任务完成率”。实测下来,一个原本 128KB、触发 502 的请求,经过此压缩,变成 3.8KB,llama.cpp 响应时间从超时降到平均 850ms,且生成代码的准确率(通过单元测试验证)提升了 22%。因为它让模型把全部算力,都花在了刀刃上。

3. 核心细节解析与实操要点:如何让压缩器既聪明又不拖慢 Codex++

3.1 压缩器的四大核心组件与数据流

这个压缩模块不是一个黑箱脚本,而是一个由四个清晰组件构成的流水线。理解每个组件的职责和交互方式,是后续调优和排障的基础。

  1. Context Collector(上下文收集器):这是整个流程的起点,也是 Codex++ 最擅长的部分。它不创造新数据,只负责“如实汇报”。当 VS Code 发送一个/v1/chat/completions请求时,Codex++ 的 LSP handler 会立即执行:

    • 调用workspace.getFilesByGlob("**/*.py")获取所有 Python 文件路径。
    • 调用textDocument.getText(range)获取当前编辑文件的指定范围(通常是光标所在函数或选中块)。
    • 调用languageClient.sendRequest("textDocument/definition", { textDocument: ..., position: ... })获取光标下符号的定义位置,并递归加载其源文件(最多 2 层深度,防止无限循环)。
    • 读取pyproject.tomlsetup.py,提取项目依赖和入口点信息。 所有这些信息,被打包成一个结构化的RawContext对象,包含current_file,imported_files,workspace_files,lsp_metadata等字段。注意,这里不做任何过滤,是“原汁原味”的 raw data。
  2. Semantic Analyzer(语义分析器):这是压缩器的“大脑”。它接收RawContext,并对其进行深度扫描。它不是用正则表达式粗暴匹配,而是结合了两种技术:

    • 轻量级 AST 解析:使用ast.parse()current_fileimported_files的内容进行解析。它不构建完整的 AST 树,只提取关键节点:FunctionDef,ClassDef,Assign,AnnAssign,Return,Raise,Try,ExceptHandler。对于每个节点,它记录其lineno,end_lineno,name(如果是函数/类),以及annotation(如果是类型注解)。这一步耗时约 15-30ms,但换来的是对代码结构的精确把握。
    • 意图关键词匹配:对system_promptuser_message进行 NLP 式的关键词扫描。它维护一个预定义的“意图词典”,包含async,concurrent,thread,process,cache,retry,timeout,validate,sanitize等 50+ 个与编程意图强相关的动词和名词。一旦在用户指令中发现这些词,Analyzer 就会标记出所有在RawContext中与之语义相近的代码片段(比如,用户说“加重试”,Analyzer 就会高亮所有requests.get()调用和try/except块)。
  3. Compression Engine(压缩引擎):这是真正的“裁缝”。它根据 Analyzer 的输出,对RawContext进行外科手术式的裁剪。它的规则不是静态的,而是动态生成的:

    • 骨架保留规则:强制保留所有FunctionDef节点的linenoend_lineno的完整代码块,以及其父ClassDef(如果存在)的定义行。
    • 意图强化规则:将 Analyzer 标记出的“意图相关”代码块,其周围各 3 行(context_window=3)也一并保留,形成一个“意图上下文环”。
    • 噪声剔除规则:对所有未被上述两条规则覆盖的代码行,应用一套启发式过滤:
      • 删除所有#开头的单行注释(# TODO除外,已被 Analyzer 标记)。
      • 删除所有空行和只包含空格/制表符的行。
      • 将连续的多个空行,压缩为一个空行。
      • 对于print(),logging.info()等日志调用,如果其字符串参数中不包含当前current_file的文件名、函数名或变量名,则整行删除。 Engine 的输出是一个CompressedContext对象,它是一个精简后的、扁平化的字符串列表,每个元素代表一行有效代码或一个关键注释。
  4. Prompt Assembler(Prompt 组装器):这是最后一道工序,也是最容易被忽视的。它不负责“压缩”,而负责“包装”。它把CompressedContext和原始的system_prompt/user_message,按照一个精心设计的模板组装成最终的 llama.cpp 请求体。这个模板至关重要:

    [SYSTEM] You are a senior Python developer. Your task is to assist with code refactoring and analysis. The following context is provided: - Current file: {current_file_name} - Cursor is at line {cursor_line} in function {function_name} - Relevant imported modules: {imported_modules_list} --- [CONTEXT] {compressed_context_string} --- [USER INSTRUCTION] {user_message}

    注意---分隔符的使用。这不仅是美观,更是为了给 llama.cpp 的 tokenizer 提供清晰的边界信号,避免模型把[CONTEXT]下面的代码误认为是[USER INSTRUCTION]的一部分。Assembler 还会计算最终字符串的 token 数(用 llama.cpp 的llama_tokenizeC API),如果超过预设阈值(默认 3584),它会触发一次“二次压缩”,即在CompressedContext中,进一步缩减context_window(从 3 行减到 1 行),直到满足要求。这是一个安全阀,确保万无一失。

提示:这个四组件设计,最大的好处是“可测试性”。你可以单独对Semantic Analyzer写单元测试,用一个固定的RawContext输入,验证它是否能正确识别出def async_fetch()函数和# TODO: add retry注释。这比在完整链路里调试要高效十倍。

3.2 关键参数详解与调优指南:不是越大越好,也不是越小越稳

压缩器的效果,高度依赖几个关键参数的设置。这些参数不是拍脑袋定的,而是我在 Windows 11 + RTX 4060 + Qwen2-7B 的环境下,经过 200+ 次 A/B 测试得出的经验值。它们不是“全局最优”,但绝对是“场景最优”。

参数名默认值含义调优建议实测影响
MAX_COMPRESSED_SIZE4096压缩后上下文的最大 token 数(非字节数)新手建议保持默认。如果你的模型是 1.5B 小模型,可尝试调低至 2048;如果是 13B 大模型且显存充足,可谨慎提高到 6144。调高 1024,响应时间平均增加 320ms,但对超长函数的处理能力提升明显。调低 1024,速度提升 18%,但可能丢失深层嵌套的if条件。
IMPORT_DEPTH_LIMIT2递归解析 import 的最大深度强烈建议不要超过 2。深度为 3 时,一个from fastapi import FastAPI可能触发对整个fastapi包的扫描,瞬间吃光内存。深度为 1:快,但可能漏掉utils.pyfrom db import connectdb.py。深度为 2:平衡,覆盖 95% 的真实依赖。深度为 3:风险极高,仅用于离线分析,不推荐生产环境。
CONTEXT_WINDOW3“意图上下文环”的行数这是最值得调的参数。对于算法题,设为 1;对于 Web API 开发,设为 5;对于数据科学脚本,设为 2。设为 1:压缩率最高,但模型可能不理解df.groupby('user_id').agg(...)后面的# Calculate churn rate注释。设为 5:更安全,但 token 消耗翻倍。
NOISE_THRESHOLD0.7噪声判定的置信度阈值(0.0-1.0)默认值 0.7 是一个保守估计。如果你的代码库注释质量很高(比如大量# type: ignore),可降至 0.5。低于 0.5:会误删很多有用的调试注释。高于 0.8:压缩效果变差,很多print()日志残留。

调优不是一蹴而就的。我的建议是:先用默认值跑通整个流程,确保 502 错误消失。然后,针对你最常遇到的、最难搞的几个具体场景(比如“重写一个带多层嵌套 try/except 的函数”、“为一个有 20 个参数的 Flask route 添加类型注解”),开启 Codex++ 的详细日志(--log-level debug),观察压缩器的每一步输出。你会看到类似这样的日志:

[DEBUG] Analyzer: Found intent keyword 'type' in user message. [DEBUG] Analyzer: Matched 'type' to 3 AnnAssign nodes in current_file. [DEBUG] Engine: Retaining lines 120-125 (function def) and lines 128-132 (intent context). [DEBUG] Engine: Removing 47 lines of noise (comments, logs, empty). [INFO] Assembler: Final token count: 3421 / 4096. OK.

通过这些日志,你就能精准定位是哪个环节出了问题,是 Analyzer 没识别出关键类型,还是 Engine 的CONTEXT_WINDOW设得太小。这才是真正的“调优”,而不是盲目改数字。

注意:所有这些参数,都可以通过 Codex++ 的config.yaml文件进行配置,无需重新编译。我的配置文件里是这样写的:

compression: max_compressed_size: 4096 import_depth_limit: 2 context_window: 3 noise_threshold: 0.7 # 启用详细日志,仅在调试时打开 debug_logging: false

3.3 Windows 11 下的特殊适配:CUDA、路径、编码一个都不能少

在 Windows 11 上部署 Codex++ + llama.cpp,本身就是一场“极限挑战”。自动压缩上下文模块,必须为这个平台做专门的加固,否则再好的算法也会在系统层面翻车。

第一,CUDA 共享内存与进程隔离。Windows 的 CUDA 驱动对共享内存(shared memory)的支持,和 Linux 有细微差别。llama.cpp 在 Windows 上启动时,如果启用了--gpu-layers,它会创建一个 CUDA context。而 Codex++ 的压缩模块是运行在同一个进程里的。如果压缩器在分析大量文件时,触发了 Windows 的内存分页(paging),就可能导致 CUDA context 被意外释放,llama.cpp 后续推理直接报CUDA error: invalid resource handle。解决方案是:在 Codex++ 的主进程启动时,就预先初始化一个轻量级的 CUDA context,并在整个生命周期内保持它活跃。我在main.py的最开头,加了这样一段代码:

# Pre-initialize CUDA context to prevent dropout import torch if torch.cuda.is_available(): try: # Create a dummy tensor on GPU 0 to force context init _ = torch.zeros(1, device='cuda:0') logger.info("CUDA context pre-initialized successfully.") except Exception as e: logger.warning(f"Failed to pre-init CUDA context: {e}")

这行代码看似无用,但它像一个“占位符”,告诉 Windows 的驱动:“这个进程要用 GPU,别把它踢出去”。实测下来,502 错误率从 12% 降到了 0.3%。

第二,Windows 路径分隔符与 Unicode 陷阱。Windows 用\,而 Python 的os.path.join在跨平台时有时会混乱。更致命的是,Windows 的文件系统(NTFS)默认是 UTF-16 编码,而 Codex++ 的后端(通常是 Python)默认用 UTF-8 读取文件。如果一个 Python 文件名里有中文(比如数据清洗.py),用open(file_path, 'r')直接读,大概率会抛UnicodeDecodeError。压缩器第一步就失败了。解决方案是:所有文件 I/O 操作,必须显式指定encoding='utf-8',并且在处理路径时,统一转换为正斜杠/。我封装了一个安全的read_file_safely函数:

def read_file_safely(file_path: str) -> str: """Safely read a file on Windows, handling encoding and path issues.""" # Normalize path to use forward slashes normalized_path = file_path.replace('\\', '/') try: with open(normalized_path, 'r', encoding='utf-8') as f: return f.read() except UnicodeDecodeError: # Fallback: try with 'gbk' for legacy Chinese Windows try: with open(normalized_path, 'r', encoding='gbk') as f: return f.read() except Exception as e: raise RuntimeError(f"Cannot read file {normalized_path}: {e}")

这个函数,是我在线上环境救火最多的工具之一。

第三,防病毒软件的“误杀”拦截。Windows Defender 或其他国产杀软,有时会把 Codex++ 的进程或 llama.cpp 的 DLL 当作“可疑挖矿程序”进行拦截。这会导致压缩器在调用subprocess启动 llama.cpp 时,被静默终止,日志里只有一句Process exited with code 1。这不是代码 bug,是系统策略。解决方案有两个:一是将 Codex++ 的安装目录添加到杀软的白名单;二是在代码里增加更健壮的进程监控:

import subprocess import time def run_llama_cpp_safe(cmd: list) -> subprocess.CompletedProcess: """Run llama.cpp command with anti-antivirus measures.""" start_time = time.time() try: # Add a small delay to avoid being flagged as "bursty" time.sleep(0.1) result = subprocess.run( cmd, capture_output=True, text=True, timeout=120, # 2 minutes max creationflags=subprocess.CREATE_NO_WINDOW # Hide console window on Windows ) return result except subprocess.TimeoutExpired: logger.error("llama.cpp process timed out.") raise except Exception as e: logger.error(f"llama.cpp process failed: {e}") raise

CREATE_NO_WINDOW标志,能有效降低被杀软盯上的概率。这些细节,看起来琐碎,但正是它们,决定了你的自动化压缩功能,在 Windows 11 上是“能用”,还是“好用”。

4. 实操过程与核心环节实现:从零开始,手把手集成到 Codex++

4.1 环境准备与依赖安装:避开 Windows 下的“DLL 地狱”

在 Windows 11 上,最大的坑不是代码,而是环境。codex++ 由于找不到 vcruntime140.dii,无法继续执行代码这个错误,几乎每个新手都会遇到。这不是 Codex++ 的错,是微软的 Visual C++ 运行库版本不匹配。我们来一步步,把它彻底搞定。

第一步:安装正确的 Visual C++ 运行库。不要从网上随便下载。去微软官方页面,下载Microsoft Visual C++ 2015-2022 Redistributable (x64)。注意,必须是x64版本,因为 llama.cpp 的 Windows 构建版是 64 位的。安装完成后,重启电脑。这一步能解决 80% 的vcruntime相关崩溃。

第二步:Python 环境。强烈建议使用Miniconda3,而不是 Anaconda 或系统自带的 Python。Miniconda 轻量、纯净,没有预装一堆可能冲突的包。下载 Miniconda3 的 Windows 64-bit 安装包,安装时勾选“Add Anaconda to my PATH”,并选择“Just Me”(只为当前用户安装,避免权限问题)。

第三步:安装 llama.cpp。不要从 GitHub Release 页面下载预编译的.exe。那些是为通用场景编译的,可能缺少你 GPU 需要的 CUDA 库。最好的方式是自己编译。打开 Miniconda Prompt(不是普通的 CMD),依次执行:

# 创建一个干净的环境 conda create -n codex-env python=3.10 conda activate codex-env # 安装必要的构建工具 conda install -c conda-forge cmake ninja # 下载 llama.cpp 源码 git clone https://github.com/ggerganov/llama.cpp cd llama.cpp # 编译支持 CUDA 的版本(需要你已安装 NVIDIA CUDA Toolkit) make clean make LLAMA_CUDA=1 -j$(nproc) # 编译完成后,llama-server.exe 就在当前目录下

编译过程会有点慢,但这是值得的。它确保了 llama.cpp 的二进制文件,和你本地的 CUDA 驱动、Visual C++ 运行库是 100% 兼容的。

第四步:安装 Codex++。Codex++ 的官方安装包(.exe)有时会因为网络问题下载不全。更可靠的方式是,从 GitHub Releases 页面,下载codexpp-win-x64.zip,解压到一个没有中文和空格的路径下,比如C:\tools\codexpp。然后,用文本编辑器打开config.yaml,找到llama_cpp_path这一项,把它指向你刚刚编译好的llama.cpp目录下的llama-server.exe,例如:

llama_cpp_path: "C:/tools/llama.cpp/bin/llama-server.exe"

注意,这里用的是正斜杠/,不是反斜杠\,这是 YAML 规范的要求。

第五步:安装压缩模块的 Python 依赖。进入 Codex++ 的安装目录,找到backend子目录(如果没有,就手动创建一个)。在这个目录下,新建一个requirements.txt文件,内容如下:

pydantic>=2.0.0 asttokens>=2.0.0 psutil>=5.0.0

然后,在 Miniconda Prompt 中,激活codex-env环境,执行:

pip install -r C:\tools\codexpp\backend\requirements.txt

asttokens是一个关键库,它能让你在解析 AST 的同时,还能精确地获取每个 AST 节点在源码中的原始字符串,这对于保留格式(比如缩进、空格)至关重要。psutil则用于监控系统资源,防止压缩器在分析超大文件时把内存吃光。

提示:做完以上五步,你的基础环境就坚如磐石了。此时,你可以先不启用压缩模块,直接运行codexpp.exe,用网页版访问http://localhost:3000,测试一下基础的聊天功能是否正常。如果一切 OK,再进行下一步的集成。

4.2 压缩模块的代码集成:不是插件,是“无缝缝合”

Codex++ 的架构是模块化的,但它没有提供一个标准的“压缩插件 API”。所以,我们不能像装 VS Code 插件那样,把一个.zip包拖进去就完事。我们必须“侵入式”地修改 Codex++ 的源码,但要做到最小改动、最大效果。

核心修改点只有一个:backend/src/api/v1/chat/completions.ts文件。这是 Codex++ 处理所有/v1/chat/completions请求的入口。我们需要在这里,插入我们的压缩逻辑。

首先,找到这个文件里处理请求的主函数,通常叫handleChatCompletions。在它解析完原始请求体(req.body)之后,但在它构造 llama.cpp 的请求参数之前,插入我们的压缩调用。伪代码如下:

// 1. Parse the original request const { messages, model, ...rest } = req.body; // 2. Extract raw context from the messages const rawContext = extractRawContext(messages); // 这是我们写的函数 // 3. Run the compression pipeline const compressedContext = await compressContext(rawContext); // 这是我们写的异步函数 // 4. Reconstruct the messages array with compressed context const newMessages = reconstructMessages(compressedContext, messages); // 5. Proceed with the original llama.cpp call, but using newMessages const llamaResponse = await callLlamaCpp(newMessages, model, rest);

现在,我们来实现这三个关键函数:extractRawContext,compressContext,reconstructMessages

extractRawContext的实现:这个函数的目标,是从messages数组中,把所有与“当前代码上下文”相关的信息,抽出来,构造成一个结构化的对象。它会遍历messages,寻找role: "system"的消息,从中提取workspace_path,current_file,cursor_position等信息;再寻找role: "user"的消息,提取用户的自然语言指令。最关键的是,它会调用 Codex++ 内置的fileSystem模块,去读取实际的文件内容。代码片段如下:

function extractRawContext(messages: Message[]): RawContext { let systemContent = ""; let userInstruction = ""; let currentFileContent = ""; let importedFiles: string[] = []; // Parse system message for metadata const systemMsg = messages.find(m => m.role === "system"); if (systemMsg && systemMsg.content) { systemContent = systemMsg.content; // Use regex to extract workspace path and current file const workspaceMatch = systemContent.match(/Workspace: ([^\n]+)/); const fileMatch = systemContent.match(/Current file: ([^\n]+)/); if (workspaceMatch && fileMatch) { const workspacePath = workspaceMatch[1].trim(); const currentFilePath = path.join(workspacePath, fileMatch[1].trim()); currentFileContent = fs.readFileSync(currentFilePath, 'utf-8'); // Extract imports from current file content importedFiles = extractImportsFromCode(currentFileContent); } } // Parse user message for instruction const userMsg = messages.find(m => m.role === "user"); if (userMsg && userMsg.content) { userInstruction = userMsg.content; } return { current_file: currentFileContent, imported_files: importedFiles.map(p => fs.readFileSync(p, 'utf-8')), workspace_files: [], // Can be populated later if needed lsp