拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从代码问答到任务执行:羲和Agent的架构设计与工程实践

从代码问答到任务执行:羲和Agent的架构设计与工程实践

1. 为什么多数AI编码助手止步于问答

先讲个真实场景。上个月我给团队搭了一套代码知识库问答机器人,效果相当能打:仓库里几千个文件,问"订单超时重试的逻辑在哪个模块"、"支付回调幂等怎么做的",模型能把文件路径、方法名、关键代码片段都给你翻出来。团队小伙伴用得挺开心,直到有人问了一句:"那你能不能直接帮我把这个定时任务跑一下,看看是不是真的有问题?"

我愣住了。问答可以,动手不行。

这个感受不是个例。过去一年我用过不少号称"AI编码助手"的工具,大多数本质上是增强版的代码搜索加代码生成。你问它问题,它给你答案;你让它改代码,它给你 diff;但你要它去把测试跑一遍、把任务调度起来、把构建结果拉回来看看,它就无能为力了。原因倒也不复杂:问答链路只需要"理解并生成文本",而任务执行链路需要"理解、决策、调用外部系统、感知结果、再决策",这条链路一旦走起来,要补的工程细节就多出一个量级。

我设计羲和(XiheAgent)的初衷,就是想把它从"一个很懂代码的聊天机器人"往前推一步,变成"一个能自己动手干活的编码助手"。这里的"干活"不是指自动补全几行代码,而是指:它能够理解你的诉求,把诉求拆解成具体动作,调用真实的工具去执行这些动作,然后根据执行结果决定下一步怎么做。代码问答是它的基本功,任务执行才是它真正值钱的地方。

这篇文章就围绕这个设计展开。我会先把"问答"和"执行"之间的鸿沟讲清楚,然后拆解羲和的整体架构,再重点讲两个核心跨越——工具调用协议的落地和执行任务时的调度与告警链路,最后聊聊我在真实环境中踩过的坑。如果你也在做类似的Agent,或者想把现有的AI编码助手往"能干活"的方向推一把,这篇应该能给你一些直接能用的思路。

2. 鸿沟到底在哪里:问答与执行的三层差异

很多人觉得,让AI从"回答代码问题"到"执行任务"不就是在后面加一个"调用函数"的步骤吗?真不是。我把它拆成三层,每一层都是一道坎。

2.1 第一层:上下文从"静态快照"变成"动态状态"

问答模式下,模型看到的是仓库的快照——代码文件、文档、索引。这些东西在回答过程中不会变。你可以把整个仓库读进上下文里慢慢分析,模型不需要关心"现在系统处于什么状态",反正它只是解释代码。

执行任务就不一样了。比如让羲和"把这个定时任务跑起来,然后告诉我结果",它面对的是一个动态系统:任务现在是什么状态?是等待中、运行中还是上次跑失败了?运行环境里有没有权限?任务跑的这台机器资源够不够?这些信息模型在静态代码里看不到,必须实时去问系统才能拿到。这意味着架构里不能只有"知识库",还得有"状态感知"这一层。

2.2 第二层:输出从"文本"变成"动作"

问答链路里,模型的输出是给人看的自然语言。你可以接受它有模糊表达,比如"这可能有问题"、"建议你检查一下XX"。但执行链路里,模型输出的是一系列明确的动作指令——调哪个接口、传什么参数、按什么顺序调。这里的容错率极低,参数传错一个字符,轻则任务跑不起来,重则把生产环境搞出问题。

所以你必须给模型设计一套严格的工具调用协议,而不是让它自由发挥输出JSON片段然后硬解析。协议里要有明确的约束:工具名、参数类型、必填项、取值范围、幂等性要求、超时时间。这些约束不是用来限制模型的,而是用来保证"模型随便一调,系统也能给出明确反馈"。

2.3 第三层:流程从"一次生成"变成"多轮闭环"

问答基本是一锤子买卖:问题进来,答案出去,结束。执行任务则是一个循环:发起动作 → 等待结果 → 分析结果 → 决定下一个动作 → 再发起。模型不再是"生成一句话",而是"驾驶一个流程"。

这个循环里最难的不是让模型"想清楚下一步",而是工程上怎么让这个循环稳定、可控地跑起来。包括:动作执行结果怎么返回给模型?失败时怎么重试?重试多少次?执行时间太长怎么处理?模型在这个循环里会不会陷入死循环?所有这些,都要求你在模型外面架设一套"执行引擎",而不是把希望全部寄托在模型本身的推理能力上。

我建羲和的时候,把这三点当成顶层设计约束来对待。也因此,初版架构就没有走"一个模型包打天下"的路线,而是拆成了三层。

3. 羲和(XiheAgent)的整体架构:三层链路的取舍

先看一张总览图,这是我最终定下来的结构,简化处理后是这样:

用户输入(自然语言 / 代码片段 / 任务指令) ↓ 第1层 理解层:意图识别 + 上下文检索(代码问答的基础) ↓ 第2层 规划层:任务拆解 + 工具选择 + 参数生成 ↓ 第3层 执行层:工具网关 + 任务调度 + 结果回传 + 告警 ↓ 用户收到:执行结果 / 代码建议 / 状态报告

这里我要解释一下每层存在的必要性,因为很多同类项目栽就栽在"只有第3层"或者"只有第1层",没有中间这个规划层。

3.1 理解层:代码问答的地基不能丢

羲和的底座还是代码问答。原因很简单:没有对代码库的深度理解,任务执行就是盲人摸象。你让AI去跑一个定时任务,它至少得先知道这个任务的代码逻辑是什么、依赖哪些配置、以往的执行记录长什么样,这些信息都来自理解层。

理解层我用了标准的RAG方案(检索增强生成),核心是这三件事:

  • 代码索引:把仓库里所有源文件解析成语义块(chunk),按函数、类、文件层级建立索引。这里我强烈建议按"函数粒度"切块,而不是按固定token数量硬切。原因很实际:模型回答问题时要引用具体函数,粒度太粗容易把不相关的代码混进上下文,粒度太细又会丢失函数间调用关系。
  • 混合检索:关键词检索(BM25)加向量检索并行做,然后做Rerank。单纯用向量检索在代码场景下不太够用,因为代码里的符号、类名、方法名往往是精确匹配才有效,比如搜"retryStrategy"这个字段名,向量检索可能给你捞一堆语义相近但完全无关的东西。
  • 调用链上下文:这个比较关键。检索到某个函数后,我会额外把它的上游调用方和下游被调函数也带进上下文。问答"这个任务为什么失败"时,模型能看到的不只是孤立函数,而是整条调用链。

这一层的产出是"模型对代码库的认知快照",是后续规划层的输入。

3.2 规划层:把"去执行"翻译成"执行什么、怎么执行"

规划层是羲和区别于普通代码问答助手的核心。它的职责是:把用户的模糊指令,翻译成一组结构化、可执行、可验证的工具调用序列。

举个例子。用户说:"帮我把订单回流任务重新跑一遍,如果失败就通知我。"规划层要做的事包括:

  1. 判断"订单回流任务"对应系统里的哪个任务(要去配置中心或调度平台查,而不是靠猜)。
  2. 判断"重新跑一遍"是触发一次新执行,还是重跑上次失败的执行。
  3. 判断"失败通知我"对应哪个渠道(企业微信、邮件、短信),并生成对应的通知参数。
  4. 把这整串逻辑输出为一个行动计划,交给执行层。

这一层最难的其实是让模型学会在"不确定"的时候停下来问人,而不是硬猜。我在规划提示词里明确写了三条规则:任务名称匹配不到时,必须给用户候选列表;参数无法从上下文推断时,必须向用户确认;涉及删除、覆盖、回滚类操作时,必须二次确认。这看起来很笨,但实测下来能省掉大量事故。

3.3 执行层:让规划真正落地

执行层是羲和的"手",它负责把规划层输出的工具调用序列真正执行掉。这一层要处理的问题特别工程化,包括:

  • 工具注册中心:有哪些工具可用,每个工具的入参出参是什么。
  • 执行网关:统一处理鉴权、限流、超时、重试。
  • 结果回传:执行结果要结构化回传给规划层,供模型判断下一步。
  • 状态记录:每次执行都要落库,方便追溯和审计。

执行层的设计直接决定了Agent"闯祸"的概率。我见过不少人做Agent时,把执行层简化成了"模型直接调API",结果模型调用一个删除接口时把整套测试数据清空了。所以执行层一定要做能力边界控制——哪些工具AI可以自主调用,哪些必须人工审批,这个红线必须在执行层卡死。

4. 工具协议设计:让模型从"给建议"变成"发指令"的关键

规划层说了要输出工具调用序列,那模型输出的格式到底长什么样?我前后迭代了三版协议,踩了不少坑,这里分享一个目前用着最顺手的版本。

参考OpenAI的Function Calling和Anthropic的Tool Use的思路,我给羲和定义了一套统一的工具调用协议,核心是一个JSON-RPC风格的请求体:

{ "tool": "workflow.trigger", "request_id": "req_8f3a2b9c", "params": { "task_name": "order_reconcile_daily", "trigger_mode": "manual", "timeout_seconds": 3600 }, "on_success": [ { "tool": "notification.send", "params": { "channel": "wecom", "message_template": "task_{task_name}_success_at_{time}" } } ], "on_failure": [ { "tool": "notification.send", "params": { "channel": "wecom", "message_template": "task_{task_name}_failed_reason_{error}" } } ] }

4.1 为什么协议里要有"成功/失败回调分支"

第一版协议我写得很简单,就是"调什么工具、传什么参数"。但实际跑下来发现一个严重问题:模型在工具调用失败后不知道怎么处理。比如它让调度平台跑任务,任务因为权限不足被拒绝,正常情况下应该换一个高权限的token重试,或者通知用户"我没有权限做这件事",但模型只会把错误信息原样返回给用户,跟个报错复读机一样。

后来我加了on_success和on_failure分支,含义是:模型在发起一个动作时,就要把成功和失败两个分支都规划好。这逼着模型提前思考"任务失败了怎么办",而不是事后补救。实测下来,加了回调分支后,羲和在执行任务时"一次跑通"的比例明显提高,因为很多低级错误在执行前就被模型自己预判到了。

4.2 工具注册表:每个工具都要有"人话描述"

工具调用协议里有一个容易被低估的点:模型要怎么知道有哪些工具可用、每个工具是干啥的?

我的做法是维护一张工具注册表,每个工具包含以下字段:

字段说明示例
name工具名,必须全局唯一workflow.trigger
description人话描述,说明工具的用途、适用场景触发一个工作流任务执行,适用于定时任务/ETL任务的手动触发、重跑
parameters参数JSON Schema,包含类型、必填、枚举、默认值task_name(string, required),timeout_seconds(int, default=3600)
permissions调用该工具所需的最小权限admin / developer / readonly
idempotent是否幂等,能否安全重试true / false
timeout默认超时时间30s

这里我想特别强调description和idempotent两个字段。

description写得越有人味,模型选对工具的概率越高。比如你不写"用于执行DolphinScheduler工作流",而是写"触发DolphinScheduler工作流执行,用于手动重跑昨天的失败任务、补数据等场景,注意该操作会真实启动一个分布式任务,耗时可能较长",模型就知道"补数据"这种诉求该用它,而不是去用另一个只查询状态的工具。

idempotent字段决定执行层能否自动重试。如果工具是幂等的(比如"发送通知"),失败后可以直接重试;如果不幂等(比如"创建订单"),失败后绝不能盲目重试,否则会造成重复数据。这一点在后面的真实事故里会讲到。

4.3 模型输出校验:不能信任模型的"自由发挥"

模型不是严格按照协议输出的,有时会漏参数,有时会编一个不存在的工具名,有时参数类型给错。所以工具协议解析后,还必须要过一道严格校验,包括三件事:

  1. Schema校验:参数是否合法、类型是否正确、必填项是否齐全。
  2. 工具存在性校验:工具名是否在注册表里。
  3. 权限校验:当前用户/会话是否有权调用该工具。

这三道校验全部通过后,执行层才会真正发起工具调用。不通过时,会直接把错误信息反馈给规划层,让模型去改,而不是硬着头皮执行。这一步很关键,因为LLM生成的JSON经常会有格式问题,你在解析层多花一点功夫,能省掉后面大量故障排查时间。

5. 任务执行链路:从DolphinScheduler调度到企业微信失败告警

工具协议设计好之后,真正能体现"任务执行"价值的是执行链路本身。这一章我拿一个典型场景来讲透:用户让羲和去触发一个DolphinScheduler调度任务,如果执行失败,自动往企业微信群里发告警。

这个场景非常真实,数据开发的同学应该都懂。传统做法是你自己打开DolphinScheduler控制台,找到工作流,点击运行,然后每隔几分钟刷一次日志。现在这个操作可以让羲和来干。

5.1 场景需求拆解:用户到底想要什么

用户的原话可能是:"把昨天跑失败的XX日报数据任务重新跑一遍,跑挂了记得在群里喊一声。"

这句话翻译成系统操作,其实是四件事:

  1. 在DolphinScheduler里找到名为"XX日报数据"的工作流。
  2. 查看它的最近一次执行记录,确认它确实是失败状态。
  3. 重新触发一次执行(注意,这里不能盲目触发,得先确认工作流是否会做幂等处理,否则重跑可能导致数据重复)。
  4. 实时监控这次执行的运行状态,如果最终还是失败,发送企业微信告警到指定群。

你看,自然语言一句话,背后是"查询—校验—触发—监控—告警"五个环节,每个环节都可能出幺蛾子。羲和要做的就是把这五个环节串起来,做成一个稳定的执行链路。

5.2 DolphinScheduler工具封装:不只是调一个API

我封装了两个DolphinScheduler相关工具:ds.query_workflow和ds.trigger_workflow。前者的作用是查询工作流列表、获取工作流定义ID、查最近执行记录;后者是真正触发执行。

封装过程中有两个细节容易被忽略,我写出来:

  • DolphinScheduler的API是分版本的。V1.3和V2.0的接口路径和鉴权方式差不少。我见过不少踩坑帖子,直接把网上搜到的API代码拿来用,结果因为版本不匹配,返回一堆莫名其妙的错误。封装的时候一定要先确认目标环境的DS版本,再选对接方案。
  • 触发执行分"直接触发"和"指定调度时间触发"两种。重跑昨天的失败任务,一般用调度时间参数指定schedule_time为昨天那个计划时间点;手动随即跑一次,才是直接点击"运行"按钮的效果。这两个语义如果搞混,跑出来的任务结果可能完全不对。

5.3 执行监控与状态机流转

任务被触发后,羲和不能干等,也不能问一次就完事。它需要进入一个状态机循环:

TRIGGERED → RUNNING → SUCCESS(链路结束,通知成功) ↓ FAILURE/TIMEOUT 重试判断(是否重试?剩余次数?) ↓ 重试 重新触发,回到 RUNNING ↓ 不重试 发送企微告警,链路结束

这个状态机我用Python写了一个简单的执行器,核心逻辑如下:

class WorkflowExecutor: def __init__(self, workflow_id, max_retry=2, poll_interval=30): self.workflow_id = workflow_id self.max_retry = max_retry self.retry_count = 0 self.poll_interval = poll_interval def run(self): while True: status = self._poll_status() if status == "SUCCESS": return self._notify_success() if status == "FAILURE": self.retry_count += 1 if self.retry_count <= self.max_retry: self._trigger_again() continue return self._notify_failure() if status == "TIMEOUT": self._notify_manual_review() return time.sleep(self.poll_interval)

这里有个很实用的判断:重试之前一定要先看失败原因再做决定。如果是代码逻辑错误(比如SQL写错了),重试多少次都白搭,应该直接告警让人来处理;如果是资源不足、并发冲突这类临时性问题,重试才有意义。这个判断逻辑在初版我没做,导致一个SQL错误的任务傻乎乎重试了两轮才告警,白白浪费了十几分钟。后来我让模型每次失败时先抓日志、分析失败原因、判断是否值得重试,再决定是否进入重试分支,效果好了很多。

5.4 企业微信告警:告警内容的模板设计

任务最终还是失败,且不打算继续重试时,就要发企业微信告警了。我封装了notification.send工具,支持企微机器人消息、企微应用消息两种通道。这里重点讲讲告警内容的设计——很多人以为告警就是把错误堆栈贴上去就行,其实不是。

有效的告警内容需要包含五件事:任务名、运行ID、失败阶段、失败原因摘要、处理建议。比如:

【羲和告警】数据日报任务执行失败 - 任务名:order_reconcile_daily - 运行ID:RUN-20241107-001 - 失败阶段:SQL执行(step=3/5,hive sql) - 失败原因:表 dwd_order_detail 分区数据未就绪,job timeout after 600s - 建议:检查上游调度dwd_order_detail是否正常产出,或等待分区可用后重跑 触发人:张三(来自羲和Agent手动触发)

原样贴堆栈的告警在群里没人愿意看,而这种告警一眼就知道是谁的事、该干什么。这个模板我是在被运维同事吐槽了两次之后改出来的。

5.5 风险控制:执行任务必须先过这一关

任务执行和代码问答最大的不同是,执行会有真实影响。我在执行链路里强制加了三个关卡,缺一不可:

  1. 第一次执行必确认:任何工具调用只要属于"会改变系统状态"的类别(触发任务、写数据库、发消息),执行前都要向用户确认一次"确认执行吗?",只有读操作(查询状态)可以不确认。这个规则听从了很多老运维的建议,宁可多一步,不可少一步。
  2. 脏数据保护:涉及重跑、补数这类操作时,羲和必须先查询目标分区的数据现状,如果已有数据且不是空表,它会提示"该分区已有数据,重跑会覆盖,是否继续?"。
  3. 操作审计:每一次工具调用、谁触发的、什么时候、参数是什么、结果如何,全部写入审计日志。这个日志在排查"这个任务是谁跑的"这种问题上,能救你的命。

6. 实测中的翻车现场与兜底设计

理论讲了不少,但真实的Agent开发从来不缺意外。这一章我写几个印象深刻的翻车现场,以及我针对性的兜底设计。每一个都是用真金白银换来的教训。

6.1 事故一:模型在企微告警内容里插入了一大段"免责声明"

第一次把企微告警接进线上群时,模型发送的消息末尾多了一行:"本告警由AI自动生成,可能存在错误,请谨慎参考,最终解释权归AI所有。"

群里瞬间就炸了。运维大哥直接私聊我:"你这是什么意思?让我别信这告警?"

后来我查了原因:模型在生成消息时,受到了提示词里"注意告知用户这是AI生成内容"的安全指令影响,自作主张加进了告警正文。这提醒了我一个问题——提示词里的安全约束一定要区分"该给用户看的"和"不该给用户看的"。告警内容是面向用户的产品文案,不是说教现场。后来我把所有面向用户的输出模板都从"模型自由发挥"改成了"模型填参数,模板渲染输出",告警语气的稳定度一下子就上来了。

6.2 事故二:重试机制差点重复跑了两遍任务

有段时间一个上游数据任务经常因为资源竞争失败。执行器第一次失败后自动重试,居然成功了。但用户后来发现,任务对应的数据表生成了两份数据。

排查后发现,DolphinScheduler的工作流配了对目标表的覆盖写策略(先删后写),但任务本身没有做幂等控制。当任务第一次实际执行了一部分、只是状态上报失败时,重试会导致数据写两遍。这个事故让我彻底接受了"幂等性必须前置判断"的原则:不确认幂等的任务,AI绝不自动重试,宁可告警给人看。

后来我把DolphinScheduler工作流按是否幂等分成了红绿两类名单。绿名单(明确幂等、可安全重试)里的任务,AI可以自动重试;红名单里的任务,执行失败直接转人工。这个红绿名单机制看起来土,但比任何花哨的提示词都管用。

6.3 事故三:上下文爆炸导致Agent"失忆"

有一次羲和在执行一个很耗时的任务,轮询状态轮了很多次,每一轮结果都往上下文里塞。跑了大概二十分钟后,模型突然开始胡言乱语,把之前确认过的「任务ID」和「企微群ID」都记串了,差点把告警消息发到另一个群里。

这不是玄学,是标准的上下文超载问题。长对话场景下,早期关键信息会被淹没。兜底方案三件事:一是"执行关键信息提取",每轮只把大象限的状态(当前状态、累计耗时、最近错误)塞进上下文,而不是原始JSON;二是关键参数"锚定"到上下文开头——任务ID、目标群ID、用户ID只写一遍,但放在系统提示词最前面,确保模型始终能看到;三是超长任务用"压缩摘要",把中间N轮轮询压缩成一行"已轮询N次,前N-1次均为RUNNING,无异常"。

这套兜底做完之后,再也没出现"告警发错群"的乌龙。

6.4 事故四:企微告警风暴:失败任务一口气刷屏

某次DolphinScheduler整个集群出问题,几十个任务集体失败。羲和按流程每个失败任务发一条企微告警,一分钟内群里被刷了一百多条同格式的消息。运维同事差点顺着网线来打我。

从那之后,我给告警模块加了聚合与去重策略:相同时间段内,同类型同根因的失败任务合并成一条聚合告警;单条告警里列任务清单和失败原因;五分钟内相同任务只上报一次,不再重复刷屏。另外还加了一个简单的自适应降噪:如果连续多次告警后,任务仍然失败,就自动降低告警频率,从"每次失败都告警"降为"每十分钟汇总一次"。这个改造上线后,企微群的存活率显著上升。

6.5 兜底设计的核心思路

追了这么多事故,我总结出一个核心兜底思路:Agent的自主权和责任成反比——AI能自主做的动作越多,兜底设计就必须越严密。具体来说,可以从六个维度兜底:

维度兜底手段
权限最小权限原则,AI默认只有只读权限,写操作必须临时授权
操作确认非幂等/破坏性操作执行前必须人工确认
重试策略幂等任务自动重试,非幂等任务转人工
上下文管理关键信息锚定、进程压缩、防止超载失忆
告警聚合、去重、降噪,避免风暴
审计全量操作留痕,支持一键回查

这六个维度不一定做得多重,但每个都不能缺。缺哪一个,对应的风险就会在某一天以事故的形式找上你。

7. 从"设计能跑"到"跑得可靠":几个关键工程判断

羲和目前的状态是:代码问答稳定可用,任务执行在限定场景里已经能承担一部分值班工作。但这一路下来,我最大的感受是——Agent能不能落地,七分在工程,三分在模型。

我认为最重要的工程判断有三个:

一是模型选型要克制。不是所有任务都需要最强的模型。羲和里,"意图识别"用轻量模型就够了,便宜又快;"规划层"才需要上强推理模型;"最终答复话术整理"又可以用轻量模型。一套任务链路里多个模型各司其职,成本能降一半,响应速度还能快一截。这比追求一个模型干所有事靠谱得多。

二是别让Agent自己决定"边界"。模型天然会给自己加戏。你以为它只会执行工具调用,它能在告警消息里给你写免责声明;你以为它会按计划执行,它会因为上下文一长就把关键参数记串。所以边界规则必须全部写在工程代码里——能不能重试、要不要确认、发不发告警,全部由代码和配置决定,而不是由模型的临场发挥决定。

三是从小切口开始落地。如果你问我"做AI编码助手,最先该接哪个任务执行能力",我会毫不犹豫地推荐"查询类任务"——查任务状态、查日志、查配置。这类操作是只读的,出不了大事,用户又高频需要,能让Agent快速积累信任感。跑通了只读任务,再慢慢加上"触发重跑"、"告警通知"这类有副作用的操作。步子迈大了,容易把整个项目做成事故现场。

最后分享一个小技巧

关于模型提示词和工具调用,最后分享一个我的个人技巧,价值不低但说的人不多。

我给羲和写工具提示词的时候,不追求"告诉模型所有可能的情况",而是刻意写一个反直觉的指令:"如果不能确定用哪个工具,就直接问用户,不要猜。"一开始团队有人担心,这不就变笨了吗?实际上,这一条指令让执行成功率反而提高了。因为模型发现自己"可以承认不确定"之后,就不硬编了,很多低级错误在源头就被拦下来。

AI编码助手这个方向的想象空间很大,但竞争也激烈。谁能从"会说话"走到"会干活",谁才能真正解决开发者的痛点。希望这篇分享能给你一些实打实的参考。有些细节如果没写透,欢迎一起讨论。

返回列表