用最土的方式搭建AI编程助手:caveman极简方案与token优化实践
发布时间:2026/10/6 9:55:27 锦皓数字建站

1. 项目缘起为什么我要折腾一个叫 caveman 的东西先说清楚 caveman 是什么。它不是一个库也不是一个框架更不是一个能直接npm install就完事的成品。caveman 是我自己给一套AI coding agent 的最小化运行方案起的代号。核心思路就一句话把大模型写代码这件事剥到只剩最原始的输入输出像原始人一样只保留火和石头其他花里胡哨的东西全部扔掉。你可能会问现在各种 AI 编程工具满天飞IDE 插件、命令行助手、云端 agent 一抓一大把为什么还要自己搭一套原因很直接token 用量失控。我用过不少现成的 agent 工具它们会在每一轮对话里塞入大量系统提示、工具描述、历史上下文、文件快照一轮下来动辄几万 token 就没了。对于我这种每天要跑几十次代码生成和重构的人来说账单涨得比代码行数还快。caveman 要解决的就是这个问题——用最少的 token 完成同样质量的代码任务。这套方案适合谁三类人。第一类是对 token 成本敏感、想自己掌控调用链的独立开发者第二类是想理解 AI coding agent 底层到底怎么运转的技术爱好者第三类是需要在内网或受限环境里跑代码助手、不想依赖外部复杂服务的团队。如果你只是偶尔让 AI 帮你写个正则那用现成工具就行caveman 对你来说属于杀鸡用牛刀。我搭这套东西的起点其实很朴素我想知道一个 AI coding agent 到底最少需要哪些组件才能跑起来。答案比我想的简单——一个能发 HTTP 请求的运行时、一个模型接口、一套文件读写工具、一个循环控制逻辑。剩下的全是锦上添花。caveman 就是把这四样东西用最土的方式拼在一起土到像原始人拿石头砸坚果但砸得开。2. 整体设计思路把 agent 拆到只剩骨架2.1 为什么选择 npx 作为入口而不是全局安装caveman 的启动方式我最终定成了npx caveman这种形式。这里有个很实际的考量AI coding agent 的依赖树通常很脏。你一旦全局安装不同项目对 Node 版本、对工具库版本的要求会互相打架尤其是涉及到 playwright 这类带浏览器二进制的依赖时全局安装简直是灾难现场。用 npx 的好处是每次运行都拉取指定版本项目之间互不污染。代价是首次启动会慢几秒但换来的是环境干净。我实测下来在 CI 环境里用 npx 比全局安装的失败率低很多因为 CI 每次都是全新容器全局安装反而要额外处理权限和缓存问题。注意npx 默认会检查本地 node_modules 里有没有对应包有就直接用本地的。如果你在项目里已经装了 caveman 的某个版本npx 不会去拉最新的这个行为要心里有数。2.2 token 控制的核心策略上下文分层caveman 最核心的设计就是上下文分层。我把每次请求要发给模型的内容分成四层固定层系统提示词描述 agent 的身份和基本规则这部分永远不变可以缓存。工具层当前可用的工具描述比如读文件、写文件、执行命令这部分也很少变。任务层当前用户的具体需求每次不同。历史层之前的对话和操作记录这部分是 token 消耗大户。大部分现成工具的问题在于历史层无限膨胀。caveman 的做法是给历史层设硬上限超过就做摘要压缩。压缩不是简单截断而是让模型自己把之前的操作总结成几句话只保留关键决策和文件变更记录。这样一轮长任务下来token 用量能控制在现成工具的三分之一左右。2.3 为什么不用现成的 agent 框架我试过几个流行的 agent 框架最后都放弃了。原因有两个。一是抽象层太厚一个简单的读文件操作要经过好几层封装出问题的时候排查链路特别长。二是默认行为太重框架会自动帮你做很多事比如自动重试、自动注入上下文、自动调用工具这些在 demo 里很美好在生产里就是不可控因素。caveman 反其道而行所有行为都是显式的。模型说要读文件我就读文件模型说要执行命令我就执行命令。没有隐式重试没有魔法注入。这样做的代价是我要自己处理错误和边界情况但换来的是完全的可预测性。对于要长期跑的自动化任务来说可预测比方便重要得多。3. 核心组件拆解与实操要点3.1 模型接口层怎么发请求最省 tokencaveman 的模型接口层就是一个薄薄的 HTTP 封装。这里有几个实操要点值得展开。第一请求体里只放必要字段。很多 SDK 会默认带上一些可选参数比如 temperature、top_p、presence_penalty 等这些字段虽然不直接消耗 token但会增加请求体积在批量调用时影响延迟。caveman 只传 model、messages、max_tokens 三个必填项其他全部省略让服务端用默认值。第二流式响应要开。流式不只是为了体验好更重要的是能在模型输出到一半时提前判断是否需要中断。比如模型开始胡言乱语或者陷入循环流式模式下我可以在收到特定模式时立刻断开连接省下后面的 token。非流式模式下你只能等它全部生成完才能处理浪费就浪费了。第三错误重试要有退避。模型接口偶尔会返回 429 或 503这时候无脑重试只会加剧问题。caveman 用的是指数退避第一次等 1 秒第二次 2 秒第三次 4 秒最多重试三次。超过三次就报错退出让上层决定怎么办。async function callModel(messages, retries 3) { for (let i 0; i retries; i) { try { const res await fetch(API_URL, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${KEY} }, body: JSON.stringify({ model: MODEL, messages, max_tokens: 4096, stream: true }) }); if (res.status 429 || res.status 503) { await sleep(Math.pow(2, i) * 1000); continue; } return res; } catch (e) { if (i retries - 1) throw e; await sleep(Math.pow(2, i) * 1000); } } }3.2 工具层文件读写和命令执行的安全边界工具层是 agent 和真实世界交互的接口。caveman 只提供三个工具读文件、写文件、执行 shell 命令。每个工具都有严格的安全边界。读文件工具限制在项目根目录内用路径规范化防止../逃逸。写文件工具同样限制目录并且默认不覆盖已存在的文件除非显式传 force 参数。执行命令工具最危险caveman 的做法是维护一个白名单只允许 git、npm、node、python 等开发相关命令其他一律拒绝。提示白名单机制听起来很安全但实际用起来会发现经常需要临时加命令。我的经验是白名单要配合日志每次拒绝一个命令都记录下来定期 review 看是不是需要加进去。不要因为怕麻烦就把白名单开得太大。这里有个容易被忽略的点命令执行的超时控制。有些命令会卡住比如等待输入的交互式程序。caveman 给每个命令设了 30 秒超时超时就 kill 进程并返回错误。这个超时值可以根据任务类型调整编译类任务可以放宽到 120 秒。3.3 循环控制agent 什么时候该停循环控制是 agent 的大脑。caveman 的循环逻辑很简单模型输出工具调用就执行输出最终答案就结束。但实际跑起来会发现两个问题。一是模型可能陷入工具调用循环比如反复读同一个文件。caveman 的做法是记录最近 N 次工具调用的签名如果发现重复就强制中断把控制权交回给用户。N 默认是 3也就是说同一个操作连续出现三次就触发熔断。二是模型可能永远不给最终答案一直在调用工具。这时候需要设一个最大轮次限制caveman 默认是 20 轮。超过 20 轮还没结束就强制停止把当前状态打印出来让用户决定下一步。这个值我调过几次10 轮对于简单任务够用复杂重构任务 20 轮比较合适再大就说明任务拆得不够细。3.4 上下文压缩怎么让历史层不爆炸上下文压缩是 caveman 最花心思的部分。我的实现是这样的当历史消息的总 token 数超过阈值默认 8000时触发压缩。压缩时把最早的一半消息拿出来让模型生成一段摘要然后用摘要替换掉这部分消息。摘要的提示词很关键我试了好几版才定下来。最终版本要求模型输出三部分已完成的操作列表、当前文件状态、待办事项。这样压缩后的上下文既保留了关键信息又大幅减少了 token。const SUMMARY_PROMPT 请将以下对话历史压缩成三部分 1. 已完成的操作列出关键动作和结果 2. 当前文件状态哪些文件被修改改了什么 3. 待办事项还没完成的任务 要求简洁不超过 300 字。;实测下来一次压缩能把 8000 token 的历史压到 800 token 左右压缩比接近 10:1。代价是丢失一些细节但对于长任务来说保留主线比保留细节重要。4. 完整实操流程从零跑通一个 caveman 任务4.1 环境准备与依赖安装caveman 的运行环境要求很简单Node.js 18 以上因为要用到原生 fetch。不需要 TypeScript不需要构建工具源码就是几个 .mjs 文件。第一步创建项目目录并初始化mkdir caveman-demo cd caveman-demo npm init -y第二步安装唯一的运行时依赖npm install dotenvdotenv 用来读取 .env 文件里的 API key。其他依赖一概不装保持最小化。第三步创建 .env 文件API_KEY你的密钥 API_URLhttps://api.example.com/v1/chat/completions MODELgpt-4o-mini注意.env 文件一定要加到 .gitignore 里。我见过太多人把密钥提交到仓库然后被扫描工具抓到。这个错误犯一次就够你受的。4.2 核心代码结构caveman 的代码分成四个文件index.mjs是入口model.mjs管模型调用tools.mjs管工具执行context.mjs管上下文压缩。每个文件职责单一加起来不到 500 行。index.mjs的主循环长这样import { callModel } from ./model.mjs; import { executeTool, TOOLS } from ./tools.mjs; import { compressIfNeeded } from ./context.mjs; let messages [{ role: system, content: SYSTEM_PROMPT }]; messages.push({ role: user, content: process.argv[2] }); for (let turn 0; turn 20; turn) { messages await compressIfNeeded(messages); const response await callModel(messages); const msg response.choices[0].message; messages.push(msg); if (!msg.tool_calls || msg.tool_calls.length 0) { console.log(msg.content); break; } for (const call of msg.tool_calls) { const result await executeTool(call.function.name, JSON.parse(call.function.arguments)); messages.push({ role: tool, tool_call_id: call.id, content: result }); } }这段代码就是 caveman 的全部核心逻辑。没有框架没有抽象就是发请求、执行工具、循环。4.3 工具定义与参数校验工具定义要符合模型接口的格式。caveman 的三个工具定义如下export const TOOLS [ { type: function, function: { name: read_file, description: 读取项目内的文件内容, parameters: { type: object, properties: { path: { type: string, description: 相对于项目根目录的路径 } }, required: [path] } } }, { type: function, function: { name: write_file, description: 写入文件默认不覆盖已存在文件, parameters: { type: object, properties: { path: { type: string }, content: { type: string }, force: { type: boolean, default: false } }, required: [path, content] } } }, { type: function, function: { name: run_command, description: 执行开发相关命令, parameters: { type: object, properties: { command: { type: string } }, required: [command] } } } ];参数校验在executeTool里做。路径要先path.resolve再检查是否在项目根目录内命令要先提取第一个词检查白名单。这些校验看起来繁琐但能挡住大部分模型幻觉导致的危险操作。4.4 一次真实任务的完整记录我拿一个真实任务跑了一遍让 caveman 帮我给一个 Express 项目加一个健康检查接口。第一轮模型输出工具调用read_file路径app.js。工具返回文件内容大约 200 行。第二轮模型输出write_file在 app.js 末尾追加了/health路由。工具执行成功。第三轮模型输出run_command命令是npm test。工具执行测试通过。第四轮模型输出最终答案说明改动内容和测试结果。循环结束。整个过程消耗 token 约 3500其中输入 2800输出 700。同样的任务我用某个现成 agent 工具跑过消耗约 12000 token。差距主要就在上下文管理上现成工具把整个项目文件树和大量无关上下文都塞进去了。5. 常见问题与排查技巧实录5.1 模型不调用工具怎么办这是最常见的问题。模型有时候会直接给你一段代码文本而不是调用 write_file 工具。原因通常是系统提示词不够明确。caveman 的系统提示词里有一句关键的话你只能通过工具修改文件直接输出代码不会产生任何效果。加上这句之后模型调用工具的概率大幅提升。如果还是不行可以在用户消息里再强调一次。我试过在任务描述末尾加请使用工具完成效果立竿见影。这算是提示词工程里的小技巧不优雅但管用。5.2 工具调用参数解析失败模型偶尔会输出格式错误的 JSON比如多了一个逗号或者少了引号。caveman 的处理是捕获 JSON.parse 的异常然后把错误信息作为工具结果返回给模型让它重新生成。通常模型看到错误信息后能自己纠正。如果连续两次解析失败就中断循环报错。这种情况说明模型状态不对继续下去也是浪费 token。5.3 命令执行卡住或输出过多命令卡住用超时解决前面说过了。输出过多是另一个问题有些命令会打印几千行日志全部塞进上下文会瞬间撑爆 token 预算。caveman 的做法是截断输出只保留前 2000 字符和后 2000 字符中间用省略号代替。这样既保留了开头和结尾的关键信息又控制了体积。提示截断阈值可以根据命令类型调整。测试命令的输出通常重要可以放宽到 5000 字符安装命令的输出基本没用可以收紧到 500 字符。5.4 常见问题速查表问题现象可能原因解决方法模型不调用工具系统提示词不明确强调只能通过工具修改文件JSON 解析失败模型输出格式错误返回错误让模型重试连续失败则中断命令执行超时交互式命令或死循环设 30 秒超时超时 kill 进程输出撑爆上下文命令输出过多截断保留首尾各 2000 字符token 用量异常高历史层未压缩检查压缩阈值和摘要提示词文件路径逃逸模型生成../路径路径规范化后检查是否在根目录内5.5 几个我踩过的坑第一个坑是忘记处理流式响应的分片。流式模式下返回的是 SSE 格式每个 chunk 是一行data: {...}最后以data: [DONE]结束。我一开始没处理分片边界导致 JSON 解析经常失败。后来加了一个缓冲区按行分割再解析问题就解决了。第二个坑是工具结果的 role 字段。不同模型接口对工具结果的格式要求不一样有的要求 role 是tool有的要求是function。caveman 一开始只支持一种格式换模型就报错。后来加了一个适配层根据模型类型自动转换格式。第三个坑是压缩后的上下文丢失了工具调用 ID。压缩时如果把带 tool_call_id 的消息压掉了后续的工具结果就找不到对应的调用接口会报错。解决办法是压缩时保留最近一轮的完整工具调用链只压缩更早的部分。6. 性能调优与扩展方向6.1 token 用量的实测数据我拿三个典型任务做了对比测试每个任务跑五次取平均。任务一是在现有文件里加一个函数任务是新建一个模块并写测试任务是重构一个函数并更新调用方。任务类型caveman 输入 tokencaveman 输出 token现成工具输入 token现成工具输出 token加函数18004006500500新建模块3200900110001100重构45001200150001400差距主要在输入 token 上caveman 平均只有现成工具的三分之一左右。输出 token 差距不大因为最终生成的代码量是差不多的。这个数据说明上下文管理才是省 token 的关键而不是模型选择。6.2 缓存策略哪些内容可以复用caveman 的固定层和工具层内容在多次调用之间是不变的这部分可以利用模型服务端的提示词缓存功能。具体做法是把固定层和工具层放在 messages 数组的最前面并且保证每次调用的这部分内容完全一致。服务端会识别出相同的前缀并缓存后续调用这部分 token 按缓存价格计费通常能便宜一半以上。注意缓存有有效期通常是几分钟到几十分钟。如果你的任务间隔很长缓存可能已经失效省不了钱。批量连续任务用缓存效果最好。6.3 多模型适配的注意事项caveman 设计上支持切换模型但不同模型的工具调用格式有差异。有的模型用tool_calls字段有的用function_call字段。有的模型要求工具结果用toolrole有的用functionrole。适配层要做的事情就是把这些差异抹平。我的做法是定义一个内部统一格式然后在调用模型前转换成目标格式收到响应后再转回内部格式。这样上层逻辑不用关心底层用的是哪个模型。适配层代码不多但能省掉很多切换模型时的调试时间。6.4 后续可以扩展的方向caveman 目前只支持单文件操作后续可以加目录遍历工具让模型能一次性读取整个目录结构。另一个方向是加记忆持久化把每次任务的摘要存到本地文件下次启动时加载这样跨会话也能保持上下文。还有一个方向是加并行工具调用模型一次输出多个工具调用时并行执行能缩短任务总时长。不过这些扩展都要谨慎每加一个功能就多一份复杂度和 token 开销。caveman 的哲学是保持最小化扩展之前先问自己这个功能真的必要吗还是只是看起来酷我个人在实际操作中的体会是AI coding agent 这个领域少即是多。大部分工具的问题不是功能不够而是功能太多导致不可控。caveman 用最土的方式证明了一个能读文件、写文件、执行命令的循环配上合理的上下文管理就能完成相当复杂的编程任务。剩下的都是锦上添花而锦上添花的东西往往是最贵的。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。