
我最近一直在折腾一个自研的 AI 编码助手项目代号羲和XiheAgent。这个项目的核心目标很直接让 AI 不只停留在“代码问答”这个层面而是能真正接手一部分开发任务从回答问题到执行操作形成闭环。取“羲和”这个名字是因为神话里羲和是驾驭太阳的神我希望这个 Agent 也能像驾驶太阳战车一样把代码理解和任务执行这两件事稳稳地串起来。这篇文章会完整记录我从需求拆解、架构设计、核心实现到踩坑排查的全过程。涉及的技术点包括 RAG 检索增强、多模型路由、工具调用、任务调度、执行安全与告警机制等。适合正在做 AI 应用、想从单纯聊天机器人往 Agent 方向进阶的开发者也适合想了解一个编码助手内部到底怎么运转的工程师朋友。我会尽量把关键决策背后的原因讲透而不是只丢一堆代码给你。1. 从“会聊天”到“能干活”羲和的定位与整体思路1.1 为什么编码助手需要一次“任务化”升级现在市面上大多数 AI 编程工具都能做到“代码问答”你问一段代码什么意思、哪里可能有 bug、该怎么优化它都能给出像模像样的回答。但用过几次你就会发现一个尴尬的现实AI 说完方案剩下的活儿还是得你自己干。改代码、跑测试、查日志、看报错、改配置这些动作全部要人为介入。尤其在团队协作里AI 的建议还要反复复制粘贴、手动验证效率损耗非常大。我设计羲和时最核心的一个判断是编码助手不能只是一个“顾问”它还得是一个“执行者”。仅做问答的助手价值天花板很清楚真正能理解意图、生成修改方案、执行命令、并把结果反馈给用户的助手才算是完成了从工具到生产力的跃迁。这也是我把项目标题定为“从代码问答到任务执行”的根本原因。1.2 整体架构两套引擎一条管线羲和的架构可以概括为“两套引擎、一条共享管线”。两套引擎分别是问答引擎QA Engine和执行引擎Execution Engine共享的部分是项目语义层Project Semantic Layer。项目语义层负责把代码库的结构信息、索引信息、变更记录统一暴露给上游模块避免问答和执行各搞一套数据导致信息不对齐。数据流是这样的用户输入问题后先进入意图识别模块判断这是一个“解释类问题”还是“操作类任务”。如果是解释类问题走问答引擎检索代码、组装上下文、生成回答如果是操作类任务走执行引擎解析任务、规划步骤、调用工具、执行并回传结果。整个管线里有一个统一的任务状态机来跟踪执行进度避免长耗时操作变成黑盒。之所以拆成两条链路而不是一个全能的 Prompt 硬解所有问题是因为问答和执行的失败模式不一样。问答的失败通常是“答非所问”“漏了关键细节”而执行的失败通常是“工具调用格式错误”“命令执行超时”“权限被拒”。把两种问题分开处理才能针对性地设计重试、诊断和反馈机制。1.3 设计原则重生态、轻定制、可替换动工之前我给自己定了三条设计原则。第一重生态优先复用成熟开源组件比如代码索引用 tree-sitter、向量检索用轻量的本地库而不是自己造轮子。第二轻定制在核心流程上做必要的定制但不过度设计比如意图识别只需要能分清楚“说一下”和“帮我改一下”就够用没必要训练一个多分类大模型。第三可替换模型层面做成多模型路由不绑定某一家大模型厂商方便根据成本和效果切换。这三条原则在后续开发中帮了大忙。尤其是“可替换”这一点后期我换了两次底层模型服务几乎零成本迁移靠的就是一开始就把模型调用封装成了统一接口而不是在业务代码里到处硬编码。2. 代码问答模块让 AI 真正读懂项目而不是瞎猜2.1 检索增强是问答的地基不是可选项如果你把一个代码文件几千行一次性塞给模型它会因为上下文过长而丢失早期的关键信息而且 token 费用也扛不住。所以羲和的问答引擎采用了检索增强生成RAG的思路先定位和问题相关的代码片段再让模型基于这些片段回答。代码检索和我们常见的文档检索有一个明显区别代码是结构化很强的文本函数定义、类声明、调用关系、注释都是天然的锚点。我最初直接用通用 Embedding 模型做向量检索效果很一般——因为代码里的标识符、缩写、驼峰命名在语义空间里互相干扰。后来做了改进用 tree-sitter 解析出函数、类、导入语句等语法单元分别建立索引检索时既做向量相似度匹配也做基于符号名的精确匹配两路结果做融合排序。这里有一个非常关键的体会代码问答的检索不能只看文本相似度还要看“这个符号在哪里被定义、在哪里被调用”。比如用户问“这个订单超时逻辑是怎么触发的”只有找到定时任务注册的地方才能给出完整链路而单独搜“超时”关键词是搜不到的。所以在索引阶段羲和会额外构建调用关系图和依赖图检索时把“被调用方”和“调用方”一起召回上下文会更完整。2.2 上下文组装从单轮问答到多文件关联检索到候选代码片段之后下一个问题是怎么把零散片段拼成一个结构完整的上下文我踩过的坑是直接把命中的几个文件片段拼接成一个超长 Prompt 丢给模型结果模型经常抓不住重点回复非常发散。后来我把上下文组装分成了三步。第一步按相关性和依赖关系给召回内容排序核心命中的代码块排最前面。第二步尽量补全代码块的“外壳”比如一个函数既要包含函数体还得包含它的签名、关键注释、所在类的简要描述这样模型才能理解上下文。第三步设置 token 预算超出预算的部分做截断策略优先保留“主命中片段 调用关系邻近片段”其余压缩成一行摘要。组装后的 Prompt 模板大概是这样的先给模型交代项目背景和任务角色然后按顺序列出“相关文件列表”“关键代码片段”“用户的原始问题”最后要求模型在回答时先引用定位到的文件路径和行号再给结论。加了这个要求之后回答的可信度明显提升不再是无中生有地编接口了。2.3 问答质量的关键控制点问答模块上线后我发现用户感知最明显的三个质量问题一是检索召回率不够真正相关的代码没找到二是模型答非所问在回答里堆砌了一堆通用知识三是回答缺少证据用户无法验证 AI 说的是否属实。针对召回率我做了一个 query 改写的小模块。用户的问题往往比较口语化比如“那个轮询任务怎么停了”直接拿这句话去做向量检索效果很差。我会先用一个轻量模型把问题改写成代码查询语言比如提取出“轮询任务”“停”这两个关键实体再结合项目里的触发性词汇“cron”“schedule”“stop”做扩展检索。这一步的收益非常明显召回率至少提升了三成。针对答非所问我给模型加了一条硬性约束必须先尝试定位到具体文件再基于文件内容回答。如果检索结果里确实没有相关信息要求模型明确说“代码库中未找到相关内容”而不是强行编一个答案。这个“宁缺毋滥”的策略对用户信任度的建立很关键。2.4 从答复到行动计划让 AI 能“接活”问答引擎只能输出文本但任务执行需要的是动作序列。所以我在问答和执行的衔接处加了一个意图识别与规划模块负责把自然语言请求转换成结构化指令。具体的做法是定义一个统一的中间表示简单来说就是一个 JSON字段包括 intent 类型、目标文件路径、操作类型、参数列表、依赖条件等。比如用户说“帮我给这个接口加上参数校验”意图识别结果可能是intent 是 modify_code目标文件由之前的问答定位到 UserController.java操作是“在指定方法前增加参数校验逻辑”参数是校验规则和错误提示文案。这个中间表示是整个系统里最核心的接口设计因为问答引擎负责产出它执行引擎负责消费它两者之间不直接传递自然语言文本避免了反复解析带来的信息损耗。我一度想直接让模型输出可执行的 shell 命令后来发现风险太大模型对文件路径、环境变量的理解经常出错一旦误操作代价很高。结构化 JSON 虽然看起来多了一层转换但胜在可控。3. 任务执行模块从建议到代劳安全是第一道门槛3.1 任务执行的核心链路设计任务执行链路分成五个环节接收结构化指令、任务分解、工具调用、执行验证、结果反馈。这里最容易出问题的是任务分解一个“帮我整理这个模块的 import 顺序”看起来很简单实际拆解后可能涉及读取文件、分析依赖、重排 import、写回文件、跑一遍语法检查这五个子任务。没有任务分解直接让模型一次生成所有操作很容易在长链路中断掉。所以我在执行引擎里引入了一个轻量级任务分解器它的作用是参考目标任务的复杂度把大任务拆成若干原子步骤。比如“优化接口性能”可以拆成定位热点函数、分析耗时原因、给出优化方案、实施修改、跑基准测试五个步骤。每个原子步骤都有独立的成功标准和回滚策略这样即使某一步失败也不会让整个任务直接报废。执行链路里我特别重视“验证环节”。工具执行完并不代表任务完成执行后的检查往往才是成败关键。比如跑完测试命令后要解析测试输出中的 passed/failed 数量改完代码后要重新做一次语法解析确认没有引入新的错误。这一步不能省否则 AI 给你改出一堆语法错误用户还得回头收拾烂摊子。3.2 工具调用与外部服务集成执行引擎需要一系列“手”来操作真实环境。羲和内置了几类基础工具终端命令执行器、文件读写工具、代码搜索工具、Git 操作工具以及 HTTP 请求工具。每个工具都遵循同一个注册规范声明名称、描述、输入参数 schema、输出格式。工具调用的设计上最重要的不是“能不能调起来”而是“模型能不能理解工具的边界”。模型经常犯的错是让它执行终端命令它却写出一个路径拼接错误让它改文件它却不知道要先读取文件内容就擅自覆盖。解决这些问题需要对工具描述做非常细致的约束。比如文件读写工具会明确标注“修改前必须调用读取接口获取当前内容修改后必须返回 diff 摘要”并且参数 schema 里强制校验文件路径是否在项目目录内。还有一点容易忽略工具的幂等性和副作用声明。有些操作重复执行会产生副作用比如新增一个文件、推送一次 Git 提交、启动一个后台任务。我在工具注册表里给每个工具标注了“是否幂等”“是否有副作用”“是否需要审批”执行引擎会根据这些元数据决定是否要事先征求用户确认。3.3 执行沙箱、权限控制与审批机制安全是任务执行模块最重要的设计维度没有之一。一个 AI 编码助手如果可以在用户电脑上随意执行命令那它离闯祸就只有一步之遥。我采用的方案是“双保险”操作层面加沙箱限制流程层面加审批门槛。沙箱方面羲和默认把工具调用限制在项目工作目录内文件读写不能越过根目录边界终端命令采取白名单策略只有像 pytest、git status、go vet 这类安全命令可以自动执行rm、sudo 这类高风险命令默认拒绝。用户可以在配置里手动放开某些权限但每次放开都需要显式确认。审批机制上我设计了三个风险等级。低风险操作读取文件、跑测试、搜索代码自动执行中风险操作修改文件、创建分支、安装依赖需要用户在界面上点一次确认高风险操作删除文件、强制执行、推送远端需要双重确认并记录审计日志。这个机制虽然会打断一些自动化流程但换来的是用户对工具的基本信任。我见过很多 Agent 项目把精力全放在“什么都能干”上却忽略了“什么都敢干”的后果。实际使用中用户对于要不要让 AI 直接动代码是非常谨慎的一套明确的风险分级和审批机制反而会让大家更愿意尝试自动执行。3.4 执行反馈与自动化告警任务执行不能只把命令跑完就结束反馈机制是否完善直接决定了这个助手好不好用。我参考了成熟调度系统的经验比如自己在日常开发里用过的 DolphinScheduler设置了一套任务状态机pending、running、success、failed、timeout、canceled。每个任务从创建到结束都挂着清晰的状态方便用户随时查看进度。为了让执行失败能第一时间触达用户羲和接入了企业微信告警。告警规则做了分级任务失败告警、超时告警、权限拦截告警。最开始我把所有失败都当成同样级别推送结果很快就被告警风暴淹没用户也麻了。后来改成只有“非预期失败”才推送高优告警比如任务状态机里明确标记了 retry 三次仍然失败而因为用户主动取消、参数错误这类可预期失败只记录到日志里。告警内容也不是简单丢一个“任务失败”四个字。我会在告警消息里带上执行器名称、失败阶段、最近一条日志、可能的原因分析、重试按钮链接。这样用户收到通知后可以直接判断要不要人工介入而不是先去翻日志再回来找上下文。4. 关键工程实现细节模型、上下文、调度与代码结构4.1 模型层设计为什么我选择多模型路由而不是单一大模型开发初期我也想过“一个强模型走天下”所有逻辑都让一个大模型处理。很快发现两个问题一是成本太高一个普通的代码检索问答每天调用几千次账单会很难看二是延迟不可控简单问题不需要一个强大模型花十几秒来回答。最终我实现了多模型路由请求进来后先用一个轻量快速的模型做意图分类和 query 改写这一步效果要求不高、速度快真正需要生成答案或规划任务时才把请求路由到能力更强的模型工具调用结果解析和错误分类的总结性任务则交给一个中等规模的模型来处理。模型路由的策略并不复杂根据任务类型、上下文长度、预期延时代价来决定。核心思路是“让合适的模型做合适的事”。这个设计还带来一个好处当某个模型服务不稳定时路由层可以自动把流量切换到备选模型系统可用性提升不少。4.2 上下文工程Token 预算到底怎么算做 AI 应用一定会碰到 token 预算的问题。羲和在这块踩过不少坑最后总结出了一套还算好用的计算逻辑。我把一次请求的 token 开销分成三部分系统提示词、检索上下文、用户问题与历史记录。系统提示词要控制在总预算的 10% 以内检索上下文是最大的变量需要动态裁剪用户原始问题必须完整保留。一个具体项目里我观察到常见的代码检索结果大概是这样的命中 5 到 8 个代码文件每个文件取关键片段后约 500 到 800 token那检索上下文就有 4000 到 6000 token。如果把项目里那些超长的大文件整个塞进去轻轻松松突破模型窗口。所以裁减策略非常重要。我在检索融合排序后会计算每个片段的“关键度分数”片段与 query 的相关性越高、处于调用链核心位置越多分数越高。按分数从高到低取前若干个片段直到接近预算上限。剩余片段只保留文件路径和一两句话摘要保证模型知道还有哪些文件相关但不会因为过长的内容而分心。4.3 轻量级任务调度的实现细节任务调度听起来是个大工程但羲和只需要一套轻量级的调度内核就够用了。我设计了一个任务队列加状态机的方式这在很多场景里都比直接硬跑一条线更好维护。每个任务提交后进入 pending 队列调度器按优先级取出运行中的任务会有一个超时控制。任务之间可以声明依赖关系比如“必须先执行测试成功才能执行代码格式化”。状态机的流转逻辑是这个模块最值得写清楚的地方。我在内部定义了一个任务事件总线任务状态变化时发布对应事件执行器、告警器、日志模块分别订阅感兴趣的事件。这样就避免了模块之间互相硬编码调用后面单独增加“推送短信通知”或者“同步到外部系统”只需要加一个订阅者即可完全不用改核心流程。超时控制有个很实用的细节不是所有操作都适合同一个超时阈值。文件读写我设置 10 秒测试执行给 3 分钟依赖安装给 5 分钟模型调用单独走流式超时。给不同工具配置独立的超时时间是减少“假卡死”的关键手段。4.4 设计模式在代码结构中的具体应用这个项目代码规模上来之后我重新审视了整体结构有意识地引入了一些设计模式来控制复杂度。很多人觉得设计模式是八股文实际上在 Agent 这种高度模块化的系统里设计模式就是用来防止代码腐化的。策略模式用在了多模型路由里不同模型厂商和不同能力等级的调用被封装成一个个策略类策略选择逻辑集中在路由上下文里新增一个模型服务商只需要加一个策略类不需要改动调用方。观察者模式用在任务状态事件上这我在调度节里提过。工厂模式用在工具注册上每个工具都是一个工厂产品工具实例有统一的初始化流程和生命周期管理避免散落各地的 new 操作。组合模式用在任务分解上一个复杂任务节点可以包含多个子任务节点子任务还能继续嵌套这让我在处理任务依赖时不需要写一大坨 if-else。代码结构清晰是有实际回报的。后期我新增了“生成代码注释”这个批量任务只新增了一个任务节点类型和两个工具现有框架完全复用整个过程一个下午就搞定了。如果当初没有做清晰的边界设计这又是一个满文件找代码改的周末。5. 实操过程从零搭建一个可用的羲和原型5.1 环境准备与核心依赖清单如果你也想动手搭一个类似的 AI 编码助手我建议先从最小原型开始而不是一上来就铺全功能。我的实践路径是先跑通“单个文件代码问答”再扩展检索再桥接执行最后加上调度和告警。基础环境上我用的是 Python 3.10相关依赖非常明确模型调用层面用兼容 OpenAI 接口的 SDK代码解析用 tree-sitter向量检索用 Qdrant 的本地模式任务调度和 API 服务用 FastAPI。前端界面因为要支持后续的跨浏览器使用所以选择了比较通用的 Web 技术栈不过这一层在最小原型里可以先不做用命令行交互足以验证逻辑。5.2 代码问答的最小实现思路一个可用的代码问答原型需要三步索引构建、检索、生成回答。索引构建是最耗时的部分我拿 tree-sitter 解析项目里的 Python/Java 文件提取函数和类定义并建立文件路径到代码块的映射。这一步如果你懒得解析语法树也可以直接用正则把代码块切出来但效果会差一些因为正则理解不了嵌套结构。检索我用的是“向量召回 关键词召回”的混合方式。向量召回处理语义相近的问题关键词召回处理精确符号名的匹配。之后把两路结果按分数加权合并选出 top-k 片段组装成上下文。有了上下文生成回答的提示词就不复杂了。我的模板里固定包含三句话第一句要求模型只基于给定代码片段回答第二句要求引用文件路径和行号第三句要求不知道时明说不知道。模板虽短但每句话都有目的不只是给模型提要求也是在降低回答出错的可能性。5.3 从问答到执行的桥接意图解析与结构化指令最小原型跑通问答之后就要考虑问答到执行的桥接了。这一步的关键是把自然语言转化为结构化指令我选择用轻量模型做意图分类再加规则兜底的组合方案。用户输入进来后先走一遍分类提示词给出两到三个候选意图及其置信度。比如“解释这个函数的作用”会被分类为 explain“给这个接口增加登录校验”会被分类为 modify。当置信度低于阈值时再走规则兜底通过关键词表匹配“改”“加”“删”“跑”“查”等动作词来猜测意图。结构化指令的 JSON schema 我设计得尽量简单intent、target_files、operation、params、constraints。其中 target_files 会在问答引擎检索结果里自动关联这样用户就不用手打文件路径了。这一步其实是整个体验的隐形加分项——AI 自己知道要在哪个文件上动手用户只需要确认。5.4 执行器与工具注册示例执行器接收结构化指令后开始调用具体工具。我这里提供一个简化但完整的思路示例工具注册表是一个字典key 是工具名称value 是工具的函数和参数 schema。当执行引擎需要调用工具时先根据工具名查找注册项然后用 LLM 生成的参数去匹配 schema校验通过后才真正执行。我拿“运行测试”工具来举例它的注册信息大致包含名称、描述、参数定义如 test_path 和 pytest_args、执行函数。执行函数内部用 subprocess 调用 pytest并捕获退出码、stdout、stderr。特别注意一下输出不能无脑全量返回给模型一旦测试输出很长token 又会被白白吃掉。我会对输出做一个截断和摘要只保留关键结论比如通过的用例数、失败的用例名和错误类型。这个“工具输出先做结构化摘要”的思路也适用于所有执行器。执行器返回给模型的不是一份原始日志而是一份干净的元信息报告。只有把工具输出变成模型可以高效消费的格式整个 AI 任务执行链路才会真正顺畅。6. 常见问题与排查经验实录6.1 问答答非所问、检索结果不相关问答模块最常见的毛病是答非所问。我排查这类问题时会先看检索召回列表里是否出现了真正相关的代码片段。如果召回的内容本身就不相关那问题往往出在 query 改写上用户口语化的问题没有转成代码世界里的表达。这时候我会调整改写提示词补充项目里的领域术语词典。如果召回内容相关但回答还是跑偏那问题大概率在上下文组装上。有时候多个片段拼接在一起模型受无关片段干扰分不清主次。我后来在上下文中增加了“主要目标片段”和“辅助片段”的标注让模型明确知道该围绕哪段来回答效果立竿见影。6.2 任务执行超时、卡死或假死执行器卡死是比答非所问更让人头疼的问题。最常见的原因有两个一是命令本身在等待输入比如某个测试进程进入了交互式提示二是子进程没有正确关闭文件描述符导致管道阻塞。排查这类问题我会先从任务日志里看当前执行到哪一步然后检查工具进程是否还在运行。解决办法是给所有外部命令调用加上 timeout 参数并且在超时后要强制杀掉整个进程树只杀掉父进程往往不够。另外所有 subprocess 调用里我设置了 stdin 直接关闭防止任何交互式等待。6.3 模型调用工具时参数格式反复出错模型生成工具参数时偶尔会把参数名拼错、类型搞错或者遗漏必填字段。这个问题在任务执行链路里几乎是必现的原因很现实模型对工具 schema 的理解并不稳定。我的处理方案是两层校验加一层自动修复。第一层校验用 Pydantic 模型解析失败立刻返回错误信息给模型第二层校验是工具自定义的业务规则比如检查文件路径是否越界、操作是否在白名单内。自动修复则是把校验失败的错误信息直接回传给模型让它基于错误信息重新生成参数。实测下来二次生成的成功率在九成以上。6.4 告警泛滥、误报和延迟告警模块上线后我很快就被“垃圾告警”教育了。最初任何失败都推送到企业微信用户一天能收到几十条最后全变成已读不处理。后来我加入了“失败重试”和“分级告警”机制每个工具都有独立的失败重试次数比如文件读取失败可以快速重试两次测试失败不重试而是直接通知只有重试后仍然失败并且任务状态是“非预期失败”时才推送高优告警。同时告警文案里要求带上任务 ID、失败阶段、相关日志和可能的影响范围。这样用户从收到消息到做出判断通常只要几秒钟不再需要去系统里翻半天。一点个人的体会和后续方向做到现在这个阶段我最大的体会是一个 AI 编码助手从“代码问答”走向“任务执行”真正的瓶颈不是模型能力而是工程化的边界控制。你得告诉它哪些能做、哪些不能做、做事的过程中每一步怎么验证、失败了怎么收场。把这些约束都明确下来AI 才能真正变成生产力工具而不是一个随时可能闯祸的实习生。最后再分享一个小细节我在做执行链路时一直坚持给每个关键操作加“可回滚”设计。文件修改前保留备份Git 操作前先确认当前分支状态批量任务执行前生成完整的计划预览让用户确认。这些机制增加了一点交互成本但对于建立用户信任来说是绝对值得的。接下来的方向我计划给羲和增加更丰富的插件机制让团队可以自定义业务工具还会尝试支持工作流级别的编排把多个任务串联成一个自动化流水线。到时候再写一篇完整的实践经验分享出来。如果你也在做类似的项目欢迎拿来一起交流踩坑心得。