这些年我在群里被问得最多的一个问题不是某个 YAML 怎么写而是我都把 Agent 接进 GitHub 了它怎么还总是做“正确”的错误决定说白了很多人不是不会写工作流而是没搞明白一件事——在 GitHub 这种靠事件驱动的动态环境里哪些环节必须用确定性编排锁死哪些环节才能交给 Agent 去“自由发挥”。这个判断边界如果画不清楚Agent 越聪明事故越离谱。我最近帮团队重做了一套发布流程感触很深。原来我们的发布靠人肉点按钮后来想加 AI 自动生成 Release Notes再往后又想让 Agent 自动给 issue 分类、自动打标签甚至自动提交 PR。听起来很美结果第一次上线就把主线搞乱了Agent 擅自 close 掉了一个还没验收的 issue又给另一个 P0 故障贴了个“低优先级”。它每一步都符合它自己的“逻辑”但完全不符合我们编排层定义好的业务状态机。问题不出在 Agent 身上出在这套系统的“确定性编排”和“Agent 判断边界”没有分层。这篇文章我就拿 GitHub 动态工作流当例子把这件事拆开讲清楚先说什么叫确定性编排再说 Agent 的判断边界到底应该设在哪几条线上最后给一个可以直接落地的 release 流程设计以及我踩过的一堆坑。1. 先搞清楚GitHub 动态工作流到底“动态”在哪里很多人一听到动态工作流就想到 Kubernetes、想到临时任务实际上 GitHub 本身就是一套非常典型的事件驱动动态系统。你 push 一个 tagPR 被 reviewissue 被加 label有人回复评论——这些事件都会让仓库状态发生转移。如果我们在每个事件上挂一段自动化的逻辑就等于把仓库建成了一台不停运转的状态机。1.1 为什么选 GitHub 当“确定性编排”的实验场GitHub 的天然优势是事件粒度清晰。push、pull_request、issues、issue_comment、milestone、workflow_dispatch每个事件的触发条件、payload 结构、权限模型都有明确规范。相比自己搭一套消息队列再写 workerGitHub 把这些脏活提前做完了。你可以把 GitHub 想成一个巨大的“事件闹钟”代码变化会响issue 状态变化会响定时器到点也会响。我们写的 Actions 工作流本质上就是给这些闹钟挂上不同的处理函数。动态体现在哪里同一个 workflow 文件在不同的事件、不同的分支、不同的 label 组合下会走出完全不同的执行路径。这就是“单文件多状态”的动态性。我见过不少团队把动态工作流写成了“定时任务”每天凌晨跑一遍把所有仓库同步一遍。这种做法不是不行但它没有吃到事件驱动的红利。真正好用的是把工作流设计成“状态驱动的管道”——比如只有带有release-candidatelabel 的 PR 被合并时才触发预发布构建只有当预发布环境跑完全部冒烟测试且人工确认后才能打 tag 进入正式发布。每个环节都有一道明确的状态闸门这才叫编排。1.2 动态环境最容易踩的坑让 Agent 去“理解”全局状态我在很多项目里看到过同一个错误把 Agent 放在工作流的中心位置让它自己去看仓库里发生了什么自己决定下一步要干什么。比如用 Agent 去 scan issue 列表再自己决定“这个 issue 应该由谁处理”。这件事表面上很酷但你很快就会撞上一堵墙Agent 的“判断”不具备可回放性。你无法保证同一个 issue、同一个上下文它两次跑出来的决策是完全一致的。再加上它能看到的信息往往是被裁剪过的你以为它看到了全局它实际上只看到了局部。于是工作流一旦开始运行你就失去了对系统行为的预测力。这就是为什么“动态”不等于“自由”。动态是说事件和状态在变而编排是说不管事件怎么变每一步的决策逻辑都是确定的。Agent 可以出现在编排里但只能出现在“兜底建议”的位置不能出现在“最终决定”的位置。2. 确定性编排到底在“确定”什么如果只说“确定性就是结果可预测”那太虚了。落到工程上确定性编排至少要锁死三样东西执行顺序、状态流转、副作用范围。2.1 说人话确定性编排是铁轨不是方向盘我把确定性编排类比成火车铁轨。火车从 A 站到 B 站中间经过哪些车站、在哪个岔道转弯这一切在设计时就定死了。你可以临时让司机减速但你不能在行驶过程中随便变出一个没有铁轨的走向。动态工作流同样如此。我们通过needs定义 job 之间的依赖通过if定义条件分支通过concurrency定义同一类任务在并发时的排队或取消策略。这些机制全都是“铁轨”保证系统在相同输入下走相同的路径产生相同级别的效果。有个很实诚的经验给工作流做回放演练。拿上次发布的 tag 和源数据把 workflow 重新跑一遍看能不能得到一模一样的构建产物和发布状态。如果回放不出来说明你的编排里藏着隐式状态——多半是某个步骤偷偷读了一个不该读的变量或者 Agent 在步骤之间悄悄“记住了”上次的运行结果。2.2 GitHub 上确定性编排的“标准零件”我们不需要引入额外框架GitHub Actions 原生就能拼出一套相当强悍的确定性编排。核心零件主要是这些on触发器定义工作流在什么事件下被唤醒jobs.id.if定义 job 是否参与本次运行jobs.id.needs定义跨 job 的执行依赖形成 DAGjobs.id.strategy.matrix把一坨任务参数化地拆成多个并行实例jobs.id.concurrency对同类任务做并发控制jobs.id.environment绑定环境保护规则比如需要人工审批jobs.id.permissions限制 GITHUB_TOKEN 能碰哪些 APIsteps.*.if在单步粒度做条件判断jobs.id.timeout-minutes给任务设置最长存活时间下面这个片段是我常用的“事件分流”写法把 gate 的逻辑放在最前面省得后面一堆步骤重复判断。name: issue-router on: issues: types: [opened, labeled] permissions: issues: write jobs: route: runs-on: ubuntu-latest steps: - name: check label id: check env: LABELS: ${{ toJSON(github.event.issue.labels.*.name) }} run: | if [[ $LABELS *bug* ]]; then echo pathbugflow $GITHUB_OUTPUT elif [[ $LABELS *feature* ]]; then echo pathfeatureflow $GITHUB_OUTPUT else echo pathtriage $GITHUB_OUTPUT fi - name: print path run: echo 当前进入 ${{ steps.check.outputs.path }}这个例子很简单但它表达的是一种编排态度先算状态再选路径。路径只由输入状态决定不依赖某个外部智能体“临场发挥”。2.3 为什么宁可“笨”一点也要坚持确定性有朋友问我如果 Agent 已经很强了为什么不让它直接在每一步做决策我的回答是因为工程系统要的从来不只是“这次能跑通”而是要“坏了能修、出事了能审计、换个人也一样能维护”。确定性编排直接带来的好处有四条可审计 每一步都有明确触发条件出问题时能回答“这一步为什么执行”可回滚 相同的状态可以重放不会因为 Agent 的随机性导致无法复现控制成本 只有条件满足时才会拉起大模型或外部工具不会让 Agent 自己逛一圈花掉一堆 token边界明确 人类 reviewer 只需要盯住少数几个闸门不需要像保姆一样盯着每个分支这里有个容易被忽略的点Agent 的“智能”是有成本的包括金钱成本和维护成本。把每个决策都交给 Agent等于让你每天去审核一堆“看似合理但未必正确”的结果时间成本最后比人工操作还高。3. Agent 的判断边界到底画在哪里先说结论在 GitHub 动态工作流里Agent 不是不能存在而是必须被当成一个“带判断能力的工具函数”来用而不是当成一个“项目协作者”来养。3.1 先分清“Agent”和“自动化脚本”很多人把 Agent 当成“加强版脚本”这其实是一开始就走偏了。普通脚本是if...then...输入什么就输出什么哪怕是并发重试结果也是可预期的。Agent 不一样它是“有状态决策主体”会做规划、会用工具、会看反馈、会改主意。举个例子一个脚本要做 issue 分类就是读 label 关键词然后打上固定 label。而一个 Agent 做同样的事可能会先读 issue 的正文、评论、关联 PR再自己“综合判断”这是什么类型的问题。听起来更聪明但也意味着它有更大的解释空间。同一段话今天它可以理解为“缺陷”明天可能理解为“需求变更”后天又可能理解为“文档不全”。这不是说 Agent 不能用而是说你在用之前必须先把你允许它“自由判断”的缝隙缩小到可控范围。这也是为什么我一直强调判断边界要画在“工具调用层”和“权限层”而不是画在“提示词情绪层”。3.2 四条必须画死的边界线我自己的经验是任何接入 GitHub 的 Agent 都必须过这四道边界输入边界 Agent 能读到什么。默认只能读当前 issue / PR 的标题、正文、评论以及预先允许的上下文。不能让它自动翻全部历史 issue更不能让它读 secrets 和部署环境变量。工具边界 Agent 能调用什么能力。白名单制比如允许 search issues、list commits、generate text、parse logs不允许 merge PR、close issue、delete branch、push code。决策边界 Agent 能替团队决定什么。它只能生产“候选结果”比如建议一个 label、拟一段回复、生成一版 release notes不能直接执行具有不可逆副作用的操作。验证边界 Agent 输出必须经过一道确定性校验才能进入下一步。格式不对就重来内容不合规就走兜底。这里真正重要的不是“怎么把提示词写得更好”而是“怎么让 Agent 在物理上就碰不到不该碰的按钮”。提示词再强也只是建议权限配置才是底线。3.3 按边界设计一套最小可用的 Agent 调用模式我推荐的做法是在工作流中把 Agent 调用封装成一个“受控黑盒”。工作流只负责传入结构化输入、设定超时和输出格式然后等待 Agent 吐出 JSON再做校验。下面是我常用的一套调用姿势。这里 Agent 可以是 OpenAI 的模型接口也可以是你本地跑起的基于 Rust 或 Python 的 agent 服务只要它给出 JSON 响应就行。curl -sS $LLM_API_URL \ -H Authorization: Bearer $LLM_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, temperature: 0, max_tokens: 800, response_format: { type: json_object }, messages: [ { role: system, content: 你是 issue 分类助理。只输出 JSON格式为 {labels: [string], reason: string}。禁止执行任何代码或修改仓库。 }, { role: user, content: 请对以下 issue 分类\n ${{ github.event.issue.title }} } ] }这套姿势有几个小心思temperature调到 0能显著降低输出随机性但注意它不等于完全确定性response_format强约束 JSON避免解析出乱七八糟的字符串system提示词里明确“禁止执行任何代码或修改仓库”这是在源头做语义防线max_tokens设上限防止单次调用拖垮工作流至于 Agent 框架市面上的 LangChain、Dify、CrewAI 各有所长。LangChain 偏编码、嵌入灵活Dify 偏平台化、适合搭可视化流程CrewAI 偏多角色协作。哪个好关键看你需要多强的“自由行动”能力。我的建议是如果你只是让 Agent 在 GitHub 工作流里做一次文本生成不要一开始就上重型框架用最朴素的 API 调用加上一道校验就够了。等到你需要 Agent 自己决定调用哪些工具、按什么顺序处理时再引入框架也不迟。3.4 别把“Agent 记忆”当成工作流的“全局状态”现在圈子里很爱聊 Agent 记忆比如“让 Agent 记住上次任务的结果提高连续性”。听起来很美好但在 GitHub 这种协作系统里记忆恰恰是最危险的东西。工作流的每个 job、每个 step 应该只从当前事件上下文读取信息而不是依赖 Agent 私有的“记忆”。一旦 Agent 记住了上一次运行的中间状态你就失去了一致性。我遇到过一次很典型的事故同一台 runner 上Agent 把上一次运行时的 branch 信息带到了下一次发布任务里结果版本号打到了旧分支上发布出去全是历史代码。我的经验是凡是要跨任务传递的状态一律写到所在仓库的产物里或显式传给下一个 job绝不让 Agent 自己“心记”。Agent 的记忆只适合在单次对话里保持思路连续性不适合作为长期业务状态存储。4. 实战一个“确定性编排 Agent 判断”的发布流程聊完理论我们来看一个可以照抄的完整方案。场景是这样的仓库每次打v*tag 时自动跑一轮 CI再把合并过的 PR 收集出来交给 Agent 生成 release notes 初稿但最终发布动作必须经过人工确认。4.1 确定目标的最终形态这个流程里我做了一个很刻意的设计Agent 只生成“初稿”不直接发版。所有可见副作用都必须有人类参与确认。这样既保留了 AI 提效又把最终判断权牢牢握在团队手里。流程大概分五段触发v*tag 的 push跑构建、测试、镜像打包这是纯确定性环节查询上一个 tag 到当前 tag 之间合并的 PR 列表这也是确定性环节把 PR 标题列表喂给 Agent生成 release notes 初稿把初稿发布为 GitHub Release 的draft等待人工审核4.2 核心 YAML 骨架你可以直接把它当成模板改一改name: release-flow on: push: tags: - v* permissions: contents: write pull-requests: read jobs: ci: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: run tests run: | make test make build env: CI: true collect-prs: needs: ci runs-on: ubuntu-latest outputs: pr_text: ${{ steps.query.outputs.pr_text }} steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - name: query merged prs id: query uses: actions/github-scriptv7 with: result-encoding: string script: | const { owner, repo, sha } context; const targetTag v${process.env.RELEASE_VERSION}; ...这里我省略了github-script里复杂的 git 比较代码但你完全可以顺手用git log --oneline找出上一个 tag 与当前 tag 之间的提交再通过 GitHub API 关联到 PR。关键是collect-prs这个 job 最终输出一段干净的纯文本比如一行一个 PR 标题。这就是 Agent 的输入边界。接下来是 Agent 生成草稿的 jobagent-draft: needs: collect-prs runs-on: ubuntu-latest environment: release-draft outputs: draft_body: ${{ steps.generate.outputs.body }} steps: - name: generate draft via agent id: generate env: LLM_API_URL: ${{ secrets.LLM_API_URL }} LLM_API_KEY: ${{ secrets.LLM_API_KEY }} PR_TEXT: ${{ needs.collect-prs.outputs.pr_text }} run: | python .github/scripts/generate_release_notes.py $PR_TEXT \ /tmp/release-body.md echo body_path/tmp/release-body.md $GITHUB_OUTPUT - name: verify output size run: | test -s /tmp/release-body.md || exit 1 - name: create draft release env: GH_TOKEN: ${{ github.token }} run: | gh release create ${GITHUB_REF_NAME} \ --draft \ --notes-file /tmp/release-body.md这个agent-draftjob 有四个关键设计用了独立environment: release-draft方便你在这个环境上配置人工审批规则Agent 结果落到本地文件再做一次“非空校验”不合格直接 failgh release create --draft创建的是草稿不会真正发布给用户整个 job 的权限只有contents: write它连修改代码的权限都没有4.3 把“最终发布”这道闸门留给人工我特意把草稿发布和正式发布分成两步。草稿创建成功后GitHub 后台会显示一个正在等待发布的 draft release。到这一步随便哪个 maintainer 打开页面看一眼 release notes点一下“发布”就是整个流程的最后一道命令。你当然可以在这个基础上再加保护比如在environment: release-draft里添加 required reviewers只有指定人 approve 后后续的 publish job 才会运行。这个设计等于把决策权和执行权都留给人Agent 只负责“先把初稿写出来”。我个人非常推荐这种模式。AI 做生成人做裁决两边都不累也都不越界。5. 踩坑记录Agent、并发、超时以及那些诡异报错说句实话理论再顺实操的时候总有一堆鸡毛蒜皮。下面这些坑全是我和团队在真实环境里踩过的挑几个有共性的分享出来。5.1 并发任务打架发布顺序全乱第一次把 concurrency 加到工作流里我是被“打”醒的。某次我们连续推了两个 tag两个 workflow 同时跑一个构建到一半另一个已经把旧版本的 release notes 上传上去了。最后发布出去的版本号是新的内容却是旧的。解决办法很简单给同一个 workflow 加并发分组让同一类的任务要么排队要么直接取消旧的。concurrency: group: release-${{ github.ref_name }} cancel-in-progress: false这个配置的意思是同一个 tag 下的发布任务只能跑一个后来的会排队而不是把前面的干掉。如果你做的是 CI 类任务可能cancel-in-progress: true更合适但发布任务一定要排队千万别手滑取消到一半的构建。5.2 Agent 输出又臭又长还带 Markdown 污染大模型生成的 release notes 经常“创造力过剩”一会儿加表格一会儿加 emoji一会儿来个“感谢所有贡献者”的漂亮话。问题是这些内容进了 GitHub Release 之后非常难维护尤其是每次生成的内容格式还都不一样。我后来把校验做成了“硬门”只允许 Agent 输出严格的章节结构其余一律丢弃。如果输出解析不到指定结构就直接用确定性模板生成 release notes。这个兜底逻辑很重要——Agent 可以掉链子但工作流不能因为 Agent 掉链子就死在那。下面是兜底逻辑的极简版import json raw response.json()[content] # Agent 返回的内容 expected_keys {summary: str, breaking: list, features: list} try: parsed json.loads(raw) if all(isinstance(parsed.get(k), expected_keys[k]) for k in expected_keys): draft_body build_from_parsed(parsed) else: raise ValueError(unexpected schema) except Exception: draft_body build_from_commits(pr_list) # 确定性模板兜底用大白话说就是Agent 生成的东西只是“候选品”校验不过就换“工厂标准件”。5.3 一堆奇怪的 RPC 报错问题出在“空会话”上我们在接入某个 agent 服务时日志里反复出现一条诡异报错agent rpc error (-1): empty sid and service name一开始以为是网络问题后来抓了调用链才发现是我们在连接握手还没建立时就发起了调用服务端拿不到会话 ID自然直接拒绝。这类报错的排查思路其实很固定先看连接初始化日志确认握手完成再看请求头里的 session/token 是否在调用前被正确赋值最后看服务端协议版本与你本地调用的版本是否一致。处理完这三步这个问题基本就消失了。把它写在这里是想说Agent 接入的过程并不神秘大部分报错都还是工程问题不要一看到 RPC 就慌。5.4 小心 Agent token 权限被放大GitHub Actions 里的GITHUB_TOKEN默认有仓库的部分权限如果你不主动降权Agent 可能就会“顺便”拥有一些本不该有的能力。比如某次我们部署的 Agent 能读写 issues结果它自己在一次文本生成时产生了一个疑似问题就顺手把 issue 的 label 改了完全没人发现直到用户投诉说标签乱了。从那以后我给自己定了一条铁律Agent 运行所在的 jobpermissions永远白名单化。不需要写代码就只给pull-requests: read需要创建 draft release才给contents: write。绝不使用默认的全部权限。permissions: contents: read issues: read pull-requests: read少一个权限就少一个安全事故。5.5 本地连 GitHub 不稳时怎么保证 workflow 正常跑还有个很务实的问题有些同事本地拉不动 GitHub 大仓库或者 push 经常超时。这种时候我会给一个朴素建议尽量把“拉取、构建、发布”都放在 Actions runner 里做本地只负责 push 一个事件剩下交给远端 workflow。比如不要本地跑完测试再上传产物而是让 Actions 从 tag 触发后自己 checkout、build、artifact本地不下载大模型依赖而是把依赖缓存配到 Actions 的 cache 里。这样既省时间也能减少大量因网络抖动造成的失败。需要注意的是给所有外部调用都配上超时和重试尤其是调用 LLM API 的步骤否则一次网络抖动就会让整个 workflow 卡到超时。5.6 常见问题速查现象可能原因处理方式Agent 输出格式忽好忽坏没有强约束 JSON schema用 response_format出格式问题就走兜底发布内容引用旧分支Agent 记住了上一次的状态跨 job 状态一律显式传递关掉隐式记忆两个 tag 同时发布内容错乱没配 concurrency按 tag 分组排队执行调 agent 服务报 RPC 空会话握手未完成就开始调用先初始化 session再看协议版本Agent 擅自修改 issue权限过大所有 Agent job 一律白名单权限workflow 经常超时外部网络不稳定显式配置 timeout-minutes 和重试6. 我把这些经验收成几条可执行的原则最后分享几条我自己定下来、至今还在用的原则。不搞宏大叙事就是纯干活用的判断标准。第一编排层永远比智能层更靠近“结果”。能让if决定的就别让大模型决定能让 GitHub 原生机制约束的就别让提示词约束。确定性是地基Agent 是在地基上搭的秋千地基不稳秋千越高越危险。第二Agent 的一切输出都是“候选方案”。设计工作流时默认每个 Agent 结果都要过一道确定性校验否则就触发兜底。你可以不信任 Agent但是不能因为不信任就让人天天盯着日志。第三权限最小化不是一句口号而是一行一行的 YAML。给 Agent 开一次写权限之前先问自己这次运行真的需要它写仓库吗如果答案不是百分之百的“是”就改成只读。我在实际项目中感受最深的一件事是系统能不能稳定运转往往不取决于 Agent 有多聪明而取决于我们给聪明留了多少边界。把这句话放到你的 GitHub 动态工作流里大概率能少修几个凌晨的线上事故。
阅读完成 · 觉得有帮助?