资讯详情

资讯详情

从零拆解AI编程代理工具链:记忆系统、代码地图与Token记账的工程实践

1. 从零拆解一套 AI 编程代理工具链的整体设计思路1.1 为什么单靠一个大模型搞不定真实项目很多人第一次接触 AI 编程都是打开一个对话窗口把需求贴进去等它吐出一段代码复制粘贴跑一下能跑就完事。这种玩法在写几十行的脚本时确实爽但一旦项目上到几千行、跨多个模块、还要持续迭代问题就全冒出来了。我自己踩过最典型的一个坑让模型改一个函数签名它改是改了但调用这个函数的另外三个文件它压根不知道结果编译直接炸。这不是模型笨而是它压根没有项目的全局视野。所以一套真正能用的 AI 编程代理工具链核心要解决的不是“让模型写代码”而是“让模型在正确的上下文里写正确的代码”。围绕这个目标我把整套工具链拆成了几个互相咬合的模块记忆系统负责跨会话保留项目知识代码地图负责给模型一张项目结构图token 记账负责控制成本不失控再加上任务编排、工具调用、结果校验等外围组件一共 9 个仓库全部 MIT 协议开源。这套东西适合谁如果你只是偶尔让 AI 帮你写个正则、改个报错那用不上但如果你在维护一个真实项目想让 AI 代理稳定地参与日常开发比如自动改 bug、补测试、重构模块那这套工具链就是为你准备的。下面我会把每个模块的设计考量、核心实现和踩坑经验都摊开讲。1.2 九个仓库的分工与依赖关系先把整体架构摆出来不然后面讲细节容易迷路。这 9 个仓库不是平铺的而是有明确的层次层级模块职责依赖方向基础层记忆存储持久化项目知识、历史决策被上层调用基础层token 记账统计每次调用的消耗被上层调用中间层代码地图解析项目结构、符号索引依赖记忆存储中间层上下文组装按任务挑选相关代码片段依赖代码地图中间层工具注册中心统一管理可调用的工具独立上层任务编排器拆解任务、调度代理依赖中间层全部上层代理运行时执行单次代理循环依赖编排器上层结果校验器检查产出是否符合预期依赖代理运行时上层配置与 CLI统一入口、参数管理依赖全部这个分层不是拍脑袋定的。最早我是把所有功能塞在一个仓库里结果改记忆模块的时候经常把代码地图搞崩测试也难写。拆开之后每个模块可以独立测试、独立发版用起来清爽很多。依赖方向严格单向下层不知道上层的存在这样替换任意一层都不会引发连锁反应。1.3 为什么全部选 MIT 协议选 MIT 而不是 GPL 或者 Apache理由很实际。这套工具链是要嵌入到别人的开发流程里的如果用了 GPL那别人公司内部用的时候法务会来找麻烦Apache 虽然宽松但专利条款那块有些团队会纠结。MIT 是最干净的几乎没有任何附加约束谁都能拿去改、拿去商用、拿去闭源集成。我做这套东西的初衷就是让更多人能用上协议上设门槛没意义。提示开源协议的选择要在项目第一天就定好后期改协议需要所有贡献者同意非常麻烦。个人项目直接上 MIT 最省心。2. 记忆系统让 AI 记住你的项目2.1 记忆到底要记什么一提到“记忆”很多人第一反应是把聊天记录存下来。但我实测下来原始对话记录几乎没用——太冗长、太嘈杂检索的时候全是噪音。真正有价值的记忆是结构化的项目知识我把它分成四类事实记忆项目用了什么框架、什么数据库、部署在哪、有哪些环境变量。这类信息相对稳定变更频率低。决策记忆为什么选了这个方案而不是那个比如“因为要兼容旧版接口所以没用新的序列化格式”。这类信息在后续重构时极其关键。模式记忆项目里的代码惯例比如错误处理统一用某个封装、日志统一走某个入口。AI 代理照着惯例写产出才像“自己人”写的。事件记忆某次改动引发了什么问题、怎么修的。这类记忆帮代理避开重复踩坑。这四类记忆的存储策略完全不同。事实和模式可以全量加载决策和事件需要按相关性检索。如果混在一起存检索质量会断崖式下跌。2.2 存储选型为什么最后用了 SQLite 加向量索引一开始我试过纯向量数据库把所有记忆转成 embedding 存进去。问题是向量检索对“精确匹配”很不友好比如我想查“项目用的什么 ORM”向量检索可能返回一堆语义相近但答非所问的片段。后来又试过纯关系型存储靠关键词匹配但用户问“数据库怎么连的”和记忆里写的“持久层初始化流程”字面不重合就检索不到。最后的方案是混合存储SQLite 存结构化字段类型、时间、标签、关联文件同时给每条记忆生成向量存到本地索引里。检索时先用 SQL 做粗筛按类型、按文件路径再用向量做精排。这样既保证了精确性又兼顾了语义召回。# 记忆写入的核心逻辑简化版 def write_memory(content, mem_type, tags, related_files): # 1. 结构化字段入库 mem_id db.insert(memories, { content: content, type: mem_type, tags: ,.join(tags), files: ,.join(related_files), created_at: now() }) # 2. 生成向量并写入本地索引 vec embed(content) vector_index.add(mem_id, vec) return mem_id # 检索时先粗筛再精排 def recall(query, mem_typeNone, top_k5): candidates db.query(memories, typemem_type) # SQL 粗筛 query_vec embed(query) scored [(m, cosine(query_vec, vector_index.get(m.id))) for m in candidates] return sorted(scored, keylambda x: -x[1])[:top_k]选 SQLite 而不是 Postgres 是因为这套工具要能离线跑、要能塞进单个项目目录里零配置启动。向量索引也是本地文件不依赖任何外部服务。这一点对隐私敏感的项目很重要——你的项目知识不该上传到任何第三方。2.3 记忆的写入时机与去重记忆写得太勤会爆炸写得太少又没价值。我的策略是事件驱动写入只在几个明确节点触发记忆写入而不是每轮对话都写。代理完成一个任务后把任务摘要和关键决策写入决策记忆。检测到代码模式变更比如新增了一个工具函数被多处调用写入模式记忆。用户显式说“记住这个”写入事实记忆。修复 bug 后把根因和修法写入事件记忆。去重是个大坑。同一个事实可能被反复写入比如“项目用 Python 3.11”可能被写十几次。我的做法是写入前先做一次相似度检查如果已有记忆的向量相似度超过 0.92就更新已有记忆的时间戳而不是新增。这个阈值是调出来的太低会误合并不同事实太高去重效果差。注意去重阈值不要设成固定值最好按记忆类型区分。事实记忆阈值可以高一点0.95事件记忆阈值低一点0.85因为事件之间本来就容易相似。2.4 记忆的衰减与清理记忆不是越多越好。一个跑了半年的项目记忆库可能积累几千条检索时噪音会越来越大。我加了一个衰减机制每条记忆有个权重初始为 1.0每次被检索命中且被实际使用就加 0.1长期没被命中的每周衰减 5%。权重低于 0.3 的记忆进入冷存储不再参与常规检索但保留可手动查询。这个机制的效果很明显。清理前检索 top5 里经常混着过时信息清理后准确率提升了一大截。不过衰减参数要谨慎调我一开始衰减太快把一些低频但关键的记忆比如“生产环境数据库不能直接改”给沉下去了后来把衰减周期拉长到两周才稳定。3. 代码地图给 AI 一张项目全景图3.1 代码地图要解决的核心问题模型改代码改错八成是因为它不知道这段代码被谁调用、依赖了谁。代码地图的本质就是把项目的符号依赖关系抽出来让模型在改一个符号前能看到它的影响范围。这跟 IDE 的“查找引用”是一个道理只不过我们要把结果喂给模型。代码地图需要产出三类信息符号索引每个函数、类、变量定义在哪个文件哪一行。引用关系谁调用了谁、谁继承了谁、谁导入了谁。文件依赖模块之间的导入关系用来判断改一个文件会波及哪些文件。有了这三类信息代理在改代码前就能先查“这个函数被哪些地方调用”然后决定是只改定义还是连带改调用点。3.2 解析方案为什么用 tree-sitter 而不是正则早期我用正则匹配函数定义结果被各种语言特性教做人。Python 的装饰器、嵌套函数、多行签名正则根本处理不干净。后来换成 tree-sitter它是增量式的语法解析器支持几十种语言解析出来的 AST 结构规整提取符号和引用非常方便。# 用 tree-sitter 提取函数定义和调用以 Python 为例 import tree_sitter_python as tspython from tree_sitter import Language, Parser PY_LANGUAGE Language(tspython.language()) parser Parser(PY_LANGUAGE) def extract_symbols(source_code): tree parser.parse(bytes(source_code, utf8)) symbols [] calls [] def walk(node): if node.type function_definition: name node.child_by_field_name(name) symbols.append({ name: source_code[name.start_byte:name.end_byte], line: name.start_point[0], type: function }) elif node.type call: func node.child_by_field_name(function) if func: calls.append(source_code[func.start_byte:func.end_byte]) for child in node.children: walk(child) walk(tree.root_node) return symbols, callstree-sitter 的好处是解析快、容错强即使代码有语法错误也能给出部分结果。这对处理正在编辑中的文件很重要——代理改到一半的代码也得能解析。3.3 增量更新别每次全量重解析项目大了之后全量解析一次可能要好几秒。代理每改一个文件就全量重解析体验会非常卡。我的做法是文件级增量更新监听文件变更只重解析变更的文件然后更新受影响的引用关系。具体来说每个文件解析结果缓存起来文件 mtime 变了才重解析。引用关系用反向索引存符号 - 引用它的文件列表。当一个文件变更时先移除它旧的所有引用再插入新的引用。这样单文件更新的复杂度是 O(该文件的引用数)跟项目总规模无关。实测下来一个 500 文件的项目全量解析约 3 秒单文件增量更新平均 20 毫秒。代理连续改十个文件增量方案总共 200 毫秒全量方案要 30 秒差距是数量级的。3.4 代码地图与记忆系统的联动代码地图和记忆系统不是孤立的。当代理查一个符号时代码地图给出结构信息记忆系统给出这个符号相关的历史决策和事件。比如代理要改一个序列化函数代码地图告诉它被 5 个地方调用记忆系统告诉它“上次改这个函数是因为要兼容旧格式改的时候注意别破坏向后兼容”。两条信息合起来代理的改动质量完全不一样。这个联动是通过符号级记忆关联实现的每条记忆可以关联若干符号检索时以符号为 key 反查记忆。关联关系在记忆写入时自动建立——如果记忆内容里提到了某个符号名就自动关联。4. Token 记账把成本管起来4.1 为什么必须做 token 记账AI 代理跑起来之后token 消耗是看不见的。一个任务可能触发几十次模型调用每次调用输入输出加起来几千 token一天下来账单可能吓你一跳。我见过有团队用代理跑批量重构一晚上烧掉几百块第二天才发现。token 记账的核心价值就是让消耗可见、可归因、可控制。记账要回答三个问题这次任务花了多少 token花在哪个环节有没有浪费4.2 记账的粒度设计记账粒度太粗没用太细又开销大。我最后定在调用级每次模型调用记一条包含时间、任务 ID、调用类型规划/生成/校验、输入 token、输出 token、模型名、耗时。任务 ID 把同一次任务的所有调用串起来这样既能看单次调用也能汇总到任务级。# 记账装饰器包住所有模型调用 def track_usage(task_id, call_type): def decorator(func): def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) usage result.get(usage, {}) db.insert(token_usage, { task_id: task_id, call_type: call_type, model: result.get(model), input_tokens: usage.get(prompt_tokens, 0), output_tokens: usage.get(completion_tokens, 0), duration_ms: int((time.time() - start) * 1000), created_at: now() }) return result return wrapper return decorator用装饰器而不是在每个调用点手写记账是为了避免遗漏。只要模型调用统一走一个入口记账就自动生效。4.3 成本归因与预算控制光记账不够还得能按维度汇总。我做了几个常用视图维度用途典型发现按任务看单个任务成本某次重构花了 8 万 token按调用类型看哪个环节最贵规划环节占了 60%按模型看模型选择是否合理简单任务用了贵模型按时间看趋势某天消耗异常高预算控制是在任务启动前设一个上限比如单任务 5 万 token。代理每完成一次调用就检查累计消耗超过 80% 时告警超过 100% 时暂停任务并提示。这个机制救过我好几次有次代理陷入循环反复调用同一个工具预算控制及时把它掐了。提示预算上限不要设得太死留 20% 余量。有些任务前期消耗少后期集中爆发卡太紧会误杀正常任务。4.4 省 token 的几个实操技巧记账做久了自然就总结出省 token 的方法上下文裁剪别把整个文件塞给模型只给相关片段。代码地图在这里帮大忙能精确定位到符号级别。缓存复用相同的前缀比如系统提示词用缓存机制能省一大笔输入 token。分级模型规划用强模型执行用便宜模型校验用中等模型。实测能省 40% 以上。输出约束让模型只输出 diff 而不是完整文件输出 token 能降一个数量级。这些技巧单独看都是小优化叠起来效果很可观。我做过对比同一批任务优化前平均 6 万 token优化后 2.3 万降了六成多。5. 任务编排与代理运行时5.1 任务拆解把大需求切成可执行的小步用户说“给用户模块加个导出功能”这是个模糊需求。代理直接上手很容易跑偏。任务编排器的职责就是把模糊需求拆成明确的、可验证的小步骤。我的拆解流程是先让模型读需求产出任务清单比如定义导出格式、实现导出函数、加路由、写测试。每个子任务标注依赖关系形成 DAG。按拓扑序执行无依赖的可以并行。拆解质量直接决定成败。我试过让模型一次性拆完结果它经常漏掉测试和错误处理。后来改成两阶段拆解先拆功能步骤再对每个功能步骤补“配套步骤”测试、文档、错误处理。这样产出完整度高很多。5.2 代理循环观察、思考、行动、校验单个子任务的执行是一个循环观察当前状态、思考下一步、执行工具调用、校验结果。这个循环要设最大轮数防止无限循环。我一般设 15 轮超过就中断并报告。def run_agent_loop(task, max_turns15): context build_context(task) for turn in range(max_turns): # 思考让模型决定下一步 decision model.plan(context) if decision.action done: return decision.result # 行动执行工具调用 observation execute_tool(decision.tool, decision.args) # 校验检查结果是否符合预期 if not validate(observation, decision.expectation): context.add_feedback(上一步结果不符合预期请调整) context.add(observation) raise MaxTurnsExceeded(task)校验环节是关键。没有校验代理会一路错下去。校验分两层工具级校验比如改文件后检查语法是否通过和任务级校验比如跑测试看是否通过。两层都过才算这个子任务完成。5.3 工具注册中心的设计代理能干什么取决于它有哪些工具。工具注册中心统一管理所有可调用工具每个工具声明名称、参数 schema、执行函数。模型通过 schema 知道有哪些工具可用、怎么调。TOOLS {} def register(name, schema): def decorator(func): TOOLS[name] {schema: schema, func: func} return func return decorator register(read_file, { type: object, properties: {path: {type: string}}, required: [path] }) def read_file(path): with open(path) as f: return f.read()工具设计有个原则粒度要适中。太细比如“读一行”会导致调用次数爆炸太粗比如“重构整个模块”又失去控制。我一般按“一个明确的原子操作”来切比如读文件、写文件、跑测试、查符号引用。5.4 结果校验器的实现校验器是最后一道防线。它检查代理产出是否符合预期不符合就打回重做。校验规则分几类语法校验改完的代码能不能解析。测试校验跑相关测试看是否通过。类型校验静态类型检查是否通过。自定义校验项目特定的规则比如“所有公开函数必须有 docstring”。校验失败时把失败信息反馈给代理让它重试。重试次数也要设上限一般 3 次。三次还不过说明任务本身可能有问题需要人工介入。6. 实操从零跑通一次代理任务6.1 环境准备与初始化假设你已经 clone 了这 9 个仓库第一步是初始化。每个仓库都有独立的 README但整体初始化有个顺序# 1. 安装基础依赖 pip install -r requirements.txt # 2. 初始化记忆库在项目根目录生成 .agent/memory.db python -m memory_store init # 3. 构建代码地图首次全量解析 python -m code_map build --root ./src # 4. 初始化 token 记账库 python -m token_ledger init # 5. 配置模型接入填 API key 和模型名 cp config.example.yaml config.yaml # 编辑 config.yaml 填入你的配置初始化完成后项目根目录会多一个.agent/目录里面是记忆库、代码地图索引、记账库。这个目录建议加到.gitignore因为它是本地状态不该提交。6.2 配置文件的几个关键参数config.yaml里有几个参数直接影响使用体验我逐个说agent: max_turns: 15 # 单任务最大轮数 budget_tokens: 50000 # 单任务 token 预算 retry_limit: 3 # 校验失败重试次数 model: planner: strong-model # 规划用强模型 executor: cheap-model # 执行用便宜模型 validator: medium-model # 校验用中等模型 context: max_file_tokens: 2000 # 单文件最多塞多少 token max_context_tokens: 8000 # 总上下文上限max_file_tokens这个参数很关键。设太大上下文被单个大文件占满其他相关信息进不来设太小模型看不到完整函数。我一般设 2000超过就按符号切分只给相关片段。6.3 跑一个真实任务给模块加导出功能配置好后跑一个实际任务试试python -m agent run 给 user 模块加一个导出为 CSV 的功能代理会依次做这些事读需求拆成子任务定义 CSV 格式、实现导出函数、加路由、写测试。查代码地图找到 user 模块的位置和相关符号。查记忆看有没有相关的历史决策比如“导出功能统一走 export 包”。逐个执行子任务每步都校验。最后跑测试通过则报告完成。整个过程你能在终端看到实时输出包括每步的 token 消耗。跑完后可以用python -m token_ledger report --task id看这次任务的详细账单。6.4 实操中遇到的三个典型问题问题一代理改错文件。有次它把导出函数写到了一个不相关的模块里。排查发现是代码地图的符号索引没更新代理查到的还是旧位置。解决方法是每次任务开始前强制刷新代码地图。问题二上下文超限。任务复杂时相关代码片段加起来超过上下文上限模型开始丢信息。解决方法是分级加载先加载最相关的不够再加载次相关的而不是一次性全塞。问题三校验误判。有次测试因为环境问题失败代理以为是代码问题反复重试浪费了大量 token。解决方法是在校验前先跑一次环境自检环境有问题直接报错而不是让代理重试。7. 常见问题与排查技巧实录7.1 记忆检索不准怎么办记忆检索不准通常有三个原因记忆质量差、检索策略单一、阈值不合理。排查顺序是现象可能原因排查方法解决检索结果答非所问记忆内容太笼统看命中的记忆原文写入时要求具体该命中的没命中阈值太高调低阈值试按类型分阈值命中太多噪音阈值太低调高阈值试加类型过滤语义相近但字面不同纯关键词匹配检查向量索引启用混合检索我踩过最深的坑是记忆写入时太随意写“改了配置”这种没信息量的话检索时完全没法用。后来强制要求记忆必须包含“什么文件、什么符号、什么决策”质量才上来。7.2 代码地图解析失败的处理tree-sitter 虽然容错强但遇到某些语法扩展比如某些框架的自定义语法还是会解析失败。处理策略是降级解析失败的文件标记为“部分解析”只提取能提取的部分同时记录警告。代理用到这些文件时会看到警告知道信息可能不全。另一个常见问题是符号重名。不同模块可能有同名函数代码地图要能区分。我的做法是用“文件路径 符号名”作为唯一 key而不是只用符号名。7.3 Token 消耗异常的排查Token 消耗突然飙升一般查这几个地方循环调用代理陷入循环反复调同一个工具。看调用日志里有没有重复模式。上下文膨胀每次调用都带上了全部历史导致输入 token 越来越大。检查上下文组装逻辑。输出失控模型输出了超长内容。检查有没有输出长度限制。模型选错简单任务用了贵模型。检查模型路由逻辑。我遇到过一次消耗异常最后发现是校验器的一个 bug 导致代理反复重试同一个失败步骤。这种问题看账单看不出来得看调用日志的时序。7.4 代理产出质量不稳定的应对产出质量不稳定根因往往是上下文不稳定。同样的任务有时给模型的信息全有时不全产出自然不一样。应对方法是固定上下文组装策略每次任务都按同样的规则挑选上下文而不是随机或按时间。这样至少能保证可复现。另一个方法是加约束。在系统提示词里明确项目的代码规范、命名约定、错误处理方式让模型有章可循。约束越具体产出越稳定。8. 这套工具链的扩展方向8.1 接入更多语言支持目前代码地图主要覆盖 Python、JavaScript、TypeScript、Go、Java。tree-sitter 支持的语言远不止这些扩展新语言只需要加一个语言包和对应的符号提取规则。我建议优先支持你项目实际用到的语言别贪多。8.2 记忆的团队共享现在的记忆是本地存储团队协作时每个人有自己的记忆库。一个自然的扩展是把记忆库放到共享存储上团队成员共享项目知识。但这会带来冲突问题——两个人同时写同一条记忆怎么办。我的想法是用 CRDT 或者简单的版本向量来解决不过这块还没动手。8.3 与 CI 流程集成代理跑完任务后可以自动触发 CI 流程把产出提交到分支、跑完整测试、生成报告。这样代理的产出就有了完整的质量门禁。集成点主要在结果校验器把本地校验扩展到 CI 校验。8.4 成本优化的持续迭代Token 成本优化是个持续过程。随着模型价格变化、任务类型变化最优策略也在变。我打算加一个自动调优模块根据历史记账数据推荐模型选择和上下文策略。不过这需要积累足够的数据才有意义目前还在观察阶段。这套工具链我从最初的一个脚本慢慢拆成 9 个仓库中间重构了好几次。最大的体会是AI 编程代理的难点不在模型而在工程。模型能力已经够用真正决定成败的是上下文管理、成本控制、结果校验这些“脏活累活”。把这些做扎实代理才能真正融入日常开发而不是个玩具。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →