普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月9日掘金专栏-百度Geek说

Harness Engineering: 让 Coding Agent 可靠完成长程任务

作者 百度Geek说
2026年4月9日 17:36

导读 introduction

Coding Agent 处理目标明确、规模可控的任务很成熟,但面对上千文件的批量迁移任务,会遇到上下文耗尽、中断无法恢复、规模放大后行为不可控等问题。本文从实际落地经验出发,提出任务拆解、并行执行、File As Progress 状态持久化、多层重试等核心设计,并结合真实场景展示完整方案。最终将这套编排经验沉淀为 meta-skill,让 Agent 自己生产长程任务的执行框架。

01 长程任务的特征

最近 Harness 这个词在 AI Coding 圈子里被频繁提起,Harness 的英文本意是缰绳,能让马往对的方向跑。放到AI Agent场景,就是模型能力很强,但需要一个工具使其能够在安全边界内被稳定地约束、引导和复用。

Coding Agent 已经能很好地处理目标明确、规模可控的任务了。但在做工程化建设的时候,有一类任务远比这复杂。比如:把 21 个前端模块的 JS 文件全部迁移到 TypeScript。对几十个模块做一轮全量 Code Review,再批量修复产出的几十上百条意见。把散落在代码各处的中文硬编码全部提取成 i18n 资源。

这类任务有三个共同特征:规模大,涉及成百上千个文件;运行时间长,一次跑不完,可能需要跨越多个会话;消耗 Token 极高,动辄几千万到上亿 Token的量级。

这就是我们说的"长程任务"。这不是什么新概念,我们在使用Agent完成较大规模的任务是很常见的。这篇文章往深处走一步,聊聊长程任务的 Harness Engineering。

02 关注的点

使用Agent完成任务,我们要关注下面的点:

效果。

任务能否完成:Agent在执行大量任务的时候可能在执行过程中中断,原因可能是Token超限、网络异常、服务中断等原因,任务停在半路。

完成的真实性:Agent 有时候并没有处理完所有文件,但它会告诉你"任务已完成",核查后才能发现。

中断后的连续性:断了之后能不能接上继续执行,接上之后的任务的执行质量会不会变差?

结果的可验证性:如何判断Agent执行的结果是正确的,当产出涉及几百上千个文件的变更,靠人工很难看过来,需要有程序化的手段去批量校验正确性。

速度。

1000 个文件逐个串行处理,即使每个文件只要 30 秒,也需要 8 个多小时。如果能 10 路并发,可能在 1 小时内搞定。

成本。

Agent 一次没做对,整个上下文的 Token 就浪费了。一个任务反复重试 3 次,成本直接翻 3 倍。更隐蔽的浪费是:Agent 在一个长会话里在长会话中逐渐偏离预期,前面消耗的几十万 Token 全部白费。

效果、速度、成本,这三者构成了长程任务的核心关注点。接下来所有的设计,都是围绕这三个目标展开的。

03 困难在哪里

从长程任务的特征出发,能推导出有三个核心困难点:

上下文耗尽。 模型的上下文窗口是有限的。当处理的文件越来越多,历史信息不断累积,上下文逐渐被填满。现在的 Agent 框架普遍带有上下文压缩能力,当上下文接近窗口上限时,自动对历史对话做摘要压缩。但压缩一定会丢失信息,每压缩一轮,前面的细节就模糊一层。随着任务推进、压缩不断叠加,即便是 Opus 这样的顶尖模型,对早期上下文的理解质量也会持续下降。你会看到 Agent 在第 50 个文件时"忘了"第 10 个文件建立的约定,或者重复犯前面已经纠正过的错误。更麻烦的是,模型在长上下文中还会出现"上下文焦虑":它感知到上下文快到上限了,就开始提前收尾、草草了事。Agent 明明还有文件没处理,却自己宣布"任务完成"。

中断要重来。 网络断开、Token 用尽、模型超时,这些不是异常,是常态。而 Agent 没有跨会话记忆。每次新对话开始,它面对的是一张白纸。如果没有任何恢复机制,中断就意味着从头再来,这不仅浪费已完成的工作,也拖慢了最终完成的速度,甚至可能进入“永远无法达到终点”的尴尬境地。

规模大了行为不可控。 单个文件做得好,不代表一千个文件都做得好。规模放大后,个别文件处理失败、输出格式不一致、生成的代码破坏构建,这些都会发生。如果一个文件的失败导致整个任务挂掉,那这个流程就不可能在生产环境中使用。

04 核心原则

要解决这些困难,需要下面四个原则。

任务拆解。 对应上下文耗尽的问题。不把所有事情塞进一个会话,而是把大任务拆成合理粒度的子任务,每个子任务是一个 Agent 能在单次会话内独立完成的工作单元。拆完之后,Agent 每次只需要关注有限的几个文件,上下文里只有当前任务需要的信息,不会被无关内容干扰。

并行执行。 对应速度的问题。拆完的几十上百个子任务,如果还是逐个执行,速度没有本质提升。必须支持多个 Agent 同时跑不同的子任务。

可续传。 对应中断要重来的问题。任务的进度必须持久化到会话之外的地方,使得任何一次中断后,新的会话都能接着上次的进度继续,而不是从零开始。

有完成条件。 对应行为不可控的问题。每个子任务必须有明确的、可程序化检查的成功标准。通过客观手段验证产出确实符合预期,如果不符合预期,给Agent失败的原因在当前的session继续修复,设定修复的轮次以及明确的停止边界,比如修复完成或者触发边界(比如Retry触发上限),标记FAILED。

任务拆解控制单次执行的复杂度,并行执行压缩整体耗时,可续传消除中断带来的沉没成本,完成条件保障产出的可信度。这些原则分别作用于效果、速度、成本三个维度。

05 理念

任务边界清晰。 每个子任务有明确的输入(处理哪些文件)、输出(产出什么、写到哪里)、约束(哪些操作绝对禁止)。子任务之间不共享状态、不交叉引用。这样做的好处是每个子任务可以独立理解、独立执行、独立验证。如果子任务之间有隐式依赖,一个任务的失败就会像多米诺骨牌一样影响其他任务。

根据任务间关系的不同,边界的实现方式分三种:

  • 无依赖,直接并行。 最简单的情况。子任务之间没有文件级别的依赖,各自处理各自的文件,互不干扰。比如做i18n 对每个文件的中文硬编码独立提取,不影响其他文件。这种情况下子任务之间不共享状态、不交叉引用,分组后直接并发执行。

  • 有依赖,拓扑排序。 子任务之间存在顺序约束,文件 A 依赖文件 B 的类型导出,B 必须先处理完,A 才能开始。JS to TS 迁移就是这种情况:被依赖的叶子文件先迁移,依赖它的文件后迁移。处理方式是在分组前先做依赖分析、按拓扑序排优先级,dispatch 时按优先级批次执行。同一优先级内的子任务仍然可以并行,但跨优先级必须串行。

  • 有冲突,物理隔离。 多个子任务可能修改同一个文件或互相影响的文件,比如 Code Review 修复时,两个 subAgent 可能同时改到同一个模块的公共配置文件。这种情况下让每个子任务在独立的 Git Worktree 中操作,各自修改互不干扰。冲突被推迟到合并阶段处理。当发生冲突时,直接使用脚本几乎不可能直接解决,代码层面的冲突往往需要理解上下文才能决定保留哪边,最终需要引入 Agent 来解决冲突。但延后处理的好处在于:所有子任务都已经执行完毕,工作区处于静止状态,Agent 面对的是一个确定的、不再变化的冲突集合,而不是在多个 Agent 同时修改的竞态中实时协调。在静止状态上解冲突,效果会好很多。只有当隔离方案确实行不通(比如子任务之间需要实时交换中间结果),才考虑 Agent Teams 这样的网状协作结构,但网状结构引入的通信开销和不确定性是显著的,因此是最后的选项。

图片

△ 不同任务边界的实现

错误在最小范围内解决。 "最小范围"是一个分层的概念。核心原则是:不将错误带到后续阶段,不将错误带出子任务。

  • 子任务内闭环。 子任务在执行过程中发现生成的代码编译不通过,应该在当前会话内尝试修复,而不是把编译错误留给后续步骤去处理。如果重试几轮仍然修不好,标记 FAILED、revert 环境,明确告诉上层"这里搞不定了"。不能让一个有问题的产出悄悄混进已完成的队列。

  • 阶段内收敛。 长程任务通常会分成几个阶段(比如"环境准备→批量执行→收尾验证")。如果子Agent的执行阶段发现了结果失败,应该在当前阶段内调整并重试,而不是带着问题进入收尾验证阶段再去补救。这样做的原因是:一旦子任务产出了一个有问题的文件,没被及时发现,下游任务基于这个文件继续工作,最后问题在验收时才暴露,导致浪费了整条链路的工作。所以阶段之间的交接必须是干净的:进入下一个阶段时,当前阶段的状态要么是"全部完成",要么是"部分完成、失败项已明确标记"。

步骤间有校验保障。 每完成一个子任务就立刻校验,不要攒到最后一次性验证。校验分两类:

  • 程序化校验:能用脚本判定的,绝不交给 Agent。**对于能用程序化验证的场景(比如 TypeScript 编译通过、成功完成构建、单元测试全部通过),这些都有明确的对错标准,用脚本自动检查,程序化校验的好处是零 Token 消耗、结果完全确定、可以无限次重复执行。

  • Evaluator 校验:需要主观判断的,用独立会话的 Agent 审核。**对于需要主观判断的场景(比如 Code Review 意见的质量),用独立的 Evaluator Agent 去审核。其中要注意的点:做事的 Agent 和评价的 Agent 必须在不同的会话进行,这是因为在同一个会话内,Agent 的历史推理过程会形成一种"自我说服"效应,影响后续的判断,让 Agent 倾向于认为自己之前的产出是正确的。打开一个全新的会话,Agent 面对的是干净的上下文,只看到产出本身,不受执行过程的干扰,得到客观的评价。甚至可以用不同的模型来做 Evaluator,比如用 Sonnet 模型进行 Code Review 的任务,再用 GPT 去做意见置信度判断(Grader),将置信结果交回给 Sonnet 去修复。跨模型的评估能引入不同的"视角",进一步降低偏见。

允许局部失败。 1000 个文件中有 5 个处理失败,不应该阻塞其他 995 个。任务编排框架要能容忍局部失败,已完成的部分照常合并产出。“失败”在实际实践中分为两种情况:

  • 确实搞不定,回退给人工。 子任务重试多次仍然无法通过校验,或者产出的结果明显错误。这种情况下 revert 工作区、标记 FAILED,留给人工处理。这是真正意义上的失败,不能强行合入。

  • 能搞定但不完美,接受有限的妥协。 比如 TS strict 迁移中,一个文件的绝大部分 any 都被正确消除了,但有一两处复杂的泛型推断 Agent 实在处理不了,留下了 // @ts-ignore 或者少量 any。编译能通过,业务逻辑没被改坏,只是没有达到 100% strict 的理想状态。这种情况可以标记为"通过但有妥协"(比如状态设为 DONE_WITH_WARNINGS),照常合入,把遗留的 any 记录下来作为后续人工优化的清单。如果把这种情况也当作硬失败来处理,revert 整个文件、标记 FAILED,导致大量文件在"99% 搞定"的状态下被反复重跑,浪费 Token。

06 技巧

在建设了大量的Long Term Task Skill实践后,总结出下面的技巧:

6.1 任务粒度

拆解的第一个问题是:拆到多细?

粒度太粗,单个子任务塞进去的文件太多,上下文又开始膨胀,回到了老问题。粒度太细,每个子任务只处理一个小文件,调度开销和 Prompt 模板本身的 Token 消耗占比过高,效率反而下降。

合理的粒度取决于三个因素:模型的上下文窗口大小、单个文件的平均规模、任务本身的推理复杂度。

我们在实践中用了一个经验公式来估算。以 JS to TS 迁移任务为例:

模型使用 Claude Sonnet,有效上下文窗口大约 200K Token。一个子任务的 Token 消耗由几部分组成:Prompt 模板(任务说明、规则约束、输出格式要求)大约 1K Token;输入文件内容,代码文件大约每行 10-20 Token,3000 行代码约 30K-60K Token;Agent 的工作过程(读文件、推理、写代码、跑验证、修复错误),这部分消耗通常是输入的 2-3 倍,因为 Agent 不是一次性处理完的,它会多轮读写文件、执行命令、检查结果,每一轮都会累积上下文,大约 60K-180K Token。加起来大约 90K-240K Token。所以 3000 行是一个让单次子任务能在上下文窗口内完成的经验上限,给多轮交互和可能的修复留有余量。但 3000 行不是一个写死的常数。不同任务的推理密度不一样:比如当需要 Agent 深入理解代码意图、评估设计合理性,推理消耗远大于输入本身,3000 行可能就偏多了。

判断粒度是否合适,有一个简单的检验标准:跑几组样本,看子任务的 Token 消耗是否经常逼近上下文窗口的 80%。如果经常逼近,说明粒度偏粗,应该缩小;如果只用到了 30%-40%,说明可以适当放大,减少调度开销。

还有一个容易忽略的点:同目录的文件应该尽量放在同一组。不是因为它们行数加起来刚好合适,而是因为同目录的文件往往共享 import、类型定义、配置文件。放在一起处理时 Agent 能看到完整的局部上下文,做出的修改更准确。

6.2 子任务的 CLI 化与并发调度

子任务不在 Agent 的对话里嵌套调用,而是作为独立的 CLI 进程执行。每个子任务是一次独立的 Agent 会话,由外部脚本启动和管理。这个设计选择带来了几个重要的好处:

Prompt 的确定性。 每个子任务的 Prompt 由 build-prompt.js 脚本根据任务参数程序化组装,包含明确的任务详情、规则约束、输入文件列表、输出格式要求、验证标准。所有子任务拿到的指令结构一致,不会因为主 Agent 在长会话中的"自由发挥"导致子任务理解偏差。

比如没有程序化构建 Prompt 时发生过这样的问题:你给主 Agent 的指令是这样的:

使用 subAgent 完成 code review 任务,任务 Prompt 如下:
---
请审查以下文件,按 error/warn/style 三级分类产出审查意见。
待审查文件:src/components/UserCard.tsx, src/components/UserList.tsx, src/components/UserDetail.tsx
---


但主 Agent 并不会原样转发。它"理解"了任务后,实际传给 subAgent 的 Prompt 变成了这样:

你需要审查以下组件代码。重点关注边界情况和错误处理。
以下是文件内容:
// === src/components/UserCard.tsx ===
import React from 'react';
... (200行代码被直接贴入)
// === src/components/UserList.tsx ===
... (150行代码被直接贴入)
// === src/components/UserDetail.tsx ===
... (300行代码被直接贴入)
请按 error/warn/style 三级分类产出审查意见。


对比发现经过主Agent的转述内容变了,把文件内容全部贴进了 Prompt 而不是让 subagent 自己去读文件,subagent 失去了渐进式发现代码结构的机会,审查变成了"看一坨代码然后给意见"而非"逐个文件深入理解再评价";还塞了属于主 Agent 自己推断的上下文,万一推断有误,subAgent 的审查方向就被带偏了。最终导致审查效果打折扣。

Token 消耗大幅降低。消除上下文累积,每个 CLI 子任务是一个全新的 Agent 会话,上下文里只有当前子任务需要的信息。如果在主 Agent 的会话中串行调度,到第 30 个子任务时,前面 29 个子任务的对话历史全部堆积在上下文中,白白消耗 Token,还会干扰 Agent 的注意力。 同时也可以减少Prompt 构建本身的消耗****,在主会话中,Agent 需要花 Token 去"想"怎么写子任务的指令,组织措辞、回忆约束条件、决定输出格式。build-prompt.js 脚本生成 Prompt 是程序化的省去这部分的Token。

并发数量可控。 CLI 进程可以由 dispatch.js 脚本自由控制并发数。资源充裕时开 10 路并发,资源紧张时降到 3 路,甚至可以根据 API 限流情况动态调整。在对话内让 Agent 自己调度并发,效果很难保证,模型往往过于谨慎,不愿意一次性开启几十、上百个并发子任务,很难真正达到高并发提速的效果。

可以通过脚本在前后插入逻辑。 子任务执行前,脚本可以做预处理(创建 Git Worktree、准备输入文件、检查前置条件);执行后,脚本可以做后处理(校验产出、更新状态、清理临时文件)。这些逻辑是确定性的,不需要 Agent 参与。

下面讲讲并发调度的具体设计。

dispatch 由两个脚本分阶段协作:dispatch.js 负责首批启动,poll.js 负责后续监控和补位。

dispatch.js 只执行一次。它读取任务清单,为前 N 个任务(N = 并发上限)完成前置准备和任务分配的的工作,比如创建 Git Worktree、生成 Prompt、启动子 Agent 进程,剩余任务标记为 pending 等待补位。以 Code Review 修复为例,每个任务启动前脚本先 git worktree add 创建隔离工作区,然后内联的 generateQuery 函数根据任务参数(文件路径、review 意见列表、分支名)程序化组装 Prompt,最后 spawn 子 Agent 进程在 Worktree 中执行。启动完成后,dispatch.js 将每个任务的状态(running/pending/failed)、PID、启动时间写回任务清单文件,然后退出。

poll.js 由主 Agent 在循环中反复调用。它是单次执行的,主 Agent 每隔一段时间调用一次,检查退出码决定是否继续。每次调用时 poll.js 做三件事:

第一,检查所有 running 状态的任务。通过 PID 判断进程是否还活着,如果进程已退出,去 Worktree 里找 fix_result.json,存在且合法就标记 success、备份结果;不存在就标记 failed、从日志末尾截取错误信息。

第二,补位启动 pending 任务。统计当前 running 的数量,如果低于并发上限,就从 pending 队列中取出任务,创建 Worktree、生成 Prompt、启动子 Agent,填满并发槽位。

第三,输出状态摘要并通过退出码传递信号。exit 0 表示还有活跃任务(pending 或 running),主 Agent 应该 sleep 后继续调用 poll.js;exit 2 表示所有任务已到终态,主 Agent 可以进入下一阶段(比如合并结果)。

主 Agent 的调度循环非常简单:

while true; do
    node scripts/poll.js --task-list task_list.json
    if [ $? -eq 2 ]; then break; fi
    sleep 60
done


整个调度过程中,Agent 只负责"审查代码并给出意见"这一步,其余的 Worktree 管理、Prompt 构建、进程监控、状态更新、补位启动全部由脚本完成。

图片

△ dispatch调度架构图

为 dispatch 接口的参数进行统一设计:--root(项目目录)、--concurrency(并发数)、--dry-run(预览模式)、--retry-failed(重试失败任务),这样所有长程任务 Skill 的调度方式都是一致的。

这套设计解决了几个问题。

调度策略:随到随补,不等齐再发。 不用等当前批次的所有任务都完成才启动下一批,而是哪个坑位空了就立刻补上新任务。因为子任务的执行时间不均匀——一个只有 3 个小文件的目录可能 20 秒就审完了,一个有复杂业务逻辑的目录可能要 3 分钟。如果按批次等齐再发,假设一批 10 个任务中 9 个在 30 秒内完成、1 个要 3 分钟,那 9 个坑位会空等 2 分半,十几批下来累计浪费的时间相当可观。随到随补策略让每个坑位始终有任务在跑,整体吞吐量最大化。

输出设计:对人和对 Agent 分两个通道。 设想一个具体的场景:你启动了 120 个 Code Review 子任务,10 路并发,预计 40 分钟跑完。你在终端前盯着输出,想知道"现在跑到哪了、有没有异常、还要多久"。半小时后进程意外中断了,一个新的 Agent 会话启动,它需要知道"哪些任务完成了、哪些失败了、从哪里继续"。

这两个受众的需求完全不同。作为工程师,需要的是一眼能扫到的进度概览,数字、比例、异常摘要。不需要看到 120 行的完整任务清单,那反而是噪音。Agent 恢复时需要的是结构化的、可程序化解析的完整状态数据,每个任务的 ID、状态、产出路径、失败原因。

所以终端输出给人看,简洁、可扫视:

[Progress] 45/120 DONE | 10 IN_PROGRESS | 3 FAILED | 62 TODO
[Speed] avg 35s/task | elapsed 28min | ETA ~22min
[Failed] group_12 (timeout), group_27 (compile error), group_33 (timeout)


结构化的完整状态写到文件里给 Agent 读,这是 File As Progress 的状态文件。dispatch 脚本不需要在终端输出里重复这些信息,Agent 恢复时直接读文件。

6.3 File As Progress

这是长程任务编排中最核心的设计。

所有进度状态持久化到文件系统。不依赖 Agent 的记忆,不依赖会话上下文,只依赖磁盘上的文件。每完成一步操作,立即写入文件。不攒批,因为 Agent 随时可能被中断。

这意味着无论 Agent 在哪一步被打断,下次启动时只需要读文件就能知道"上次跑到哪了"。新会话开始时,Agent 不需要任何历史上下文,它只需要:读任务清单文件,看哪些是已完成的、哪些还没开始、哪些失败了需要重试,然后从断点继续。

文件载体可以是 TSV(简单任务,人可读)、JSON(复杂任务,支持嵌套元数据),甚至纯文本。形式不重要,重要的是"一切状态都在文件里"这个约束。

6.4 任务状态设计

有了 File As Progress,还需要一套状态机来描述每个子任务的生命周期:

TODO → IN_PROGRESS → DONE
                   → FAILED
                   → SKIPPED


状态设计的核心理念是:仅凭当前状态就能决定下一步做什么。恢复逻辑不需要知道"之前发生了什么",不需要回放历史,只需要读到"当前状态是什么",就能推导出续传策略。这要求每个状态都是自描述的,它本身就携带了"接下来该怎么办"的信息。

基于这个理念,可以根据任务的复杂度设计更精细的状态,实现更精确的续传。比如一个有"分析→执行→校验"三步的子任务,粗粒度状态只有 TODO/DONE/FAILED,中断在"执行"阶段时只能从头重来。如果细化为:

TODO → ANALYZING → ANALYZED → EXECUTING → EXECUTED → VERIFYING → DONE
                                                               → FAILED


那恢复时可以精确判断:状态停在 ANALYZED,说明分析已经完成,直接从执行阶段开始;停在 EXECUTED,说明执行完了但没来得及校验,直接跑校验。每多一个状态,就多一个可以精确恢复的断点,减少重复工作。****细粒度状态必须搭配对应的持久化产物,本质还是File As Progress的思路,****每个中间状态都需要对应一个落盘的文件,例如ANALYZE将结果写入到文件,EXECUTING才能立即恢复回来。

但状态也不是越多越好。每个状态都需要对应一个持久化的检查点(比如一个中间文件或一条状态记录),维护成本不低。实际设计时,建议在任务执行时间较长、或者某个步骤有明确的中间产物可以复用的地方增加状态。对于 10 秒就能跑完的子任务,TODO/DONE/FAILED 三个状态就足够了,即使中断了,重跑的成本也很低。

最简单的恢复逻辑是:找到所有 TODO 和 FAILED 的任务,继续执行,已经 DONE 的跳过。

但有一个细节需要特别注意:IN_PROGRESS 状态的残留处理。如果 Agent 在执行某个任务的过程中被中断,这条任务的状态会停留在 IN_PROGRESS,但实际上没有任何 Agent 在处理它。恢复时必须判断这个 IN_PROGRESS 到底是"真的在跑"还是"跑到一半挂了"。

判断的依据是产出物而非状态本身。具体分几步:

第一步,检查是否有预期的产出文件存在。每个子任务在开始时就应该明确"完成后会产出什么文件",比如 TS 迁移任务会产出 .ts 文件,Code Review 任务会产出 .review.json 文件。

第二步,如果产出文件存在,检查内容是否合法。不是简单的"文件非空"就行,而是要做完整性校验:TS 文件能不能通过编译、JSON 文件能不能解析、Review 意见的格式是否符合 schema。如果校验通过,说明 Agent 其实做完了,只是状态没来得及更新。直接将状态更新为 DONE。

第三步,如果产出文件不存在,或者内容不合法,说明这是半成品。这时需要清理工作区。清理的关键是"能找到哪些是半成品"。我们的做法是:每个子任务在独立的 Git Worktree 中操作,所有修改都发生在这个隔离的工作区里。判断半成品的方法是:在 Worktree 中执行 git statusgit diff,所有未提交的变更就是半成品。清理更为简单,git worktree remove 丢掉整个 Worktree 目录,工作区回到执行前的干净状态。如果没有使用 Worktree,而是在主分支上操作,那就需要在任务开始前记录当前的 commit hash,半成品清理时 git checkout <hash> -- <files> 恢复。

完成清理后,把状态重置为 TODO,等待下次调度。

6.5 多轮重试

subagent 失败是正常的。超时、输出格式错误、生成的代码无法编译,这些都会发生。将失败进行分层,不同层次对应不同的重试策略。

内层:恢复会话。 子 Agent 因为网络异常、进程崩溃、compress 错误等原因异常退出,任务本身没有错。dispatch 脚本记录了这次执行的 conversationId,检测到异常退出后,恢复同一个会话说"继续",Agent 从断点接着跑。这相当于续传,降低了成本。比如 6 个文件改了 4 个时进程崩溃,恢复同一个会话后 Agent 直接从第 5 个继续。

中层:带反馈重试。 子 Agent 正常完成了,但产出不满足校验条件。开一个新的子 Agent 会话,把错误信息作为上下文喂进去,针对性修复。比如 tsc --strict 报了 3 个类型错误,把完整报错信息带给新会话,Agent 只修这 3 处,不重新改造整组。限制 2-3 次。达到上限仍然失败,revert 工作区、清理环境、标记 FAILED。到此为止,子任务层面不再做更多尝试。

外层:主 Agent 重新调度。 中层耗尽后留下了一批 FAILED 的文件。要不要对这些文件重新 dispatch 给子 Agent 再来一轮?这需要在主 Agent 层面去决策。重新 dispatch 意味着为这些文件重新分组、生成新的 Prompt、启动新的子 Agent,相当于把它们当作全新的子任务再跑一遍。这里需要权衡成本和时间:如果 FAILED 的文件只有两三个,重新 dispatch 一轮的代价不高,值得尝试;如果 FAILED 了几十个,可能说明任务规则本身有问题,盲目重跑只是浪费 Token,不如先排查原因再决定。

判断走哪层,看产出文件的状态,不依赖解析 Agent 的文本输出。文件是否存在、内容是否合法,是稳定的、可程序化检查的依据。

图片

△ 多轮重试分层

07 示例

上面的内容是从抽象的层面去介绍存在的困难点以及如何去解决。下面用两个真实落地的场景来具体展示它们如何组合成完整的执行方案。

7.1 全量 Code Review

场景: 21 个前端模块做一轮全量代码审查,产出审查意见并批量修复。

任务拆解: 按模块分组,每个模块内按目录归类文件,单文件源码行数超过 MAX_LINES(默认 500) 则独立成组。分组时同目录的文件放在一起,因为它们往往有共享的 import 和类型依赖,放在一起审查质量更高。

其中MAX_LINES 的选取逻辑:每个 chunk 的 token 构成为:源文件正文(主要消耗):agent 执行时从磁盘读入源文件,平均 1 行 ≈ 14 tokens(按 50 字符/行、3.5 字符/token 估算)

固定开销:prompt 模板、规则、文件元信息、review 输出及 agent 中间步骤,约 8,000 tokens

以 500 源码行为例,单 chunk 总消耗约 20,000 tokens,占 Claude Sonnet(200K)窗口的 10%,为并发子任务的对话历史和输出留出充足余量。

并行执行:dispatch 脚本以 CLI 方式启动多路 subAgent 并发审查,每个 subagent 读取自己对应的inputs/{chunkId}-input.json,审查完毕后直接将 issue 列表写入segments/{chunkId}.json。主 Agent 通过检查 segment 文件是否存在来判断任务是否完成。

校验保障: 审查意见的质量属于主观维度,需要独立的 Evaluator Agent 校验。架构变成三个角色:主 Agent 编排、subAgent 执行审查和修复、Evaluator Agent 对意见做批判性评估。Evaluator 的 Prompt 要带有挑战性语气,主动挑毛病而非寻找优点。

续传: 审查进度写入 TSV 文件。中断后恢复时,读取 TSV 文件定位到当前 Phase,跳过已完成的模块,继续处理未完成的。

7.2 JS to TS 迁移

场景: 将整个项目的 JavaScript/JSX 文件批量迁移到 TypeScript/TSX。

任务拆解: 按同目录归组,累计行数不超过 3000 行,单文件超过上限时独立成组。但这个任务有一个额外的约束:文件间存在依赖关系。被依赖的叶子文件必须先迁移完成,依赖它的文件才能开始。所以分组时还需要按依赖拓扑序排优先级。

完成条件: 这个场景可以完全用程序化验证。每个文件迁移完后,用 Babel AST 对比确保逻辑结构不变,用 tsc 做类型检查确保编译通过。两项都通过才标记为 DONE。

错误隔离: 某个文件迁移失败,不影响其他文件。失败的文件保留原始的 JS 版本,状态标记为 FAILED。整个项目在部分文件未迁移的情况下依然可以正常构建(因为 TypeScript 项目可以混用 JS 和 TS)。

File As Progress:migration-tasks.tsv 记录每个文件的迁移状态。每次启动按顺序检查:.agent.env 存在且含 token → migration-tasks.tsv 已生成 → 无 IN_PROGRESS 残留 → 进度是否全部完成,从第一个不满足的条件对应的 Phase 继续执行。每个 Phase 对应独立的 reference 文件,Agent 只读当前 Phase 所需的指令。

08 从经验到框架:Skill for Skill

在实现了上述一系列长程任务后,我们发现每个任务的骨架结构是高度一致的:

<skill-name>/
├── SKILL.md                    # Phase 定义 + 会话恢复检测 + 完成标准
├── scripts/
│   ├── discover.js             # 扫描目标,生成任务清单(幂等)
│   ├── dispatch.js             # 读清单,分组,并发调度 subagent
│   ├── build-prompt.js         # 程序化构建子任务 Prompt
│   ├── poll.js                 # 轮询子任务状态 + 补位启动
│   ├── merge.js                # 收集子任务结果,合并为最终产物
│   └── status.js               # 查询整体进度
├── references/
│   ├── phase0_setup.md         # 环境配置指令
│   ├── phase1_analyze.md       # 分析规划指令
│   ├── phase2_dispatch.md      # 批量执行指令
│   └── phase3_finalize.md      # 收尾验证指令
└── evals/
    └── evals.json              # 评估用例


这就引出了一个更进一步的思路:能不能把长程任务的编排经验本身也做成一个 Skill?

我们做了一个叫 long-term-task-orchestration 的 meta-skill(元技能)。它不直接执行任何业务任务,而是教 Agent 如何创建新的长程任务 Skill。当你告诉 Agent"我需要一个批量做 X 的 Skill",它会读取这个元技能的 reference 文件,按照模板生成完整的 SKILL.md、scripts 目录、references 目录,自动包含 Phase 设计、状态管理、并发调度、恢复逻辑。

这是用 Agent 来强化 Agent 的工作能力。不是让 Agent 做一次任务,而是让 Agent 生产出能反复做这类任务的工具,节省每一次长程任务都需要工程师亲自编排的成本。

可以从仓库地址(github.com/hixuanxuan/… Code环境下安装,安装命令如下,会将long-term-task-orchestrationskill-eval(用于评测)一并安装。

npx skills add hixuanxuan/long-running-agent-tasks -y


long-term-task-orchestration 配合 skill-creator 一起使用。你只需要用自然语言描述任务目标,Agent 会自动完成从骨架生成到脚本填充的全过程。示例prompt:

/long-term-task-orchestration 创建skill实现React Compiler迁移并下线全部memo。


Agent 会自动生成完整的 Phase 设计、脚本目录、状态管理和恢复逻辑,并自动启动skill-eval评测,将 body 的评测结果可视化展示给用户并询问是否需要继续,确认后启动循环修复的过程,确保Skill的 workflow 是可以稳定执行的。工程师不需要关心背后的并发调度、断点续传、重试分层这些编排细节。当出现任务类型是对大量同类目标执行相同的操作,并逐个验证结果,可以使用这个这个meta-skill帮助你快速完成Skill的创建,并使用创建的Skill在项目中高效稳定的完成任务。

09 结尾

Harness 中的每一个环节,都隐含了一个"当前模型做不到"的假设。随着模型能力提升,这些假设会逐渐过期。

做Harness Engineering 是在模型能力和工程可靠性之间找到合适的边界。模型每一次进化,这个边界都会移动:曾经需要脚本控制的环节,可能下一代模型就能自主处理了。但"确定哪些环节该交给模型、哪些该留在框架里"这个判断本身,不会因为模型变强而消失。每当新模型出现,重新审视这个边界,去掉一个环节,观察对结果的影响。

Harness Engineering 是团队基础设施建设的一部分,解决 Agent 完成大规模任务时的不确定性,并提供可量化的结果评估能力。

昨天以前掘金专栏-百度Geek说

我用 Go 重写了一个 OpenClaw 框架:这就是 GoClaw

作者 百度Geek说
2026年3月31日 11:56

导读 introduction

GoClaw 是一个用 Go 语言编写的个人 AI 助手,灵感来自 OpenClaw(原 Clawdbot/Moltbot)。它运行在本地服务器上,通过 WebSocket/HTTP 暴露服务,支持多种消息channel(Telegram、WhatsApp、飞书、QQ、Slack 等)接入。

全文8593字,预计阅读时间11分钟

如果 OpenClaw 代表了一种 Agent 设计思路,那么 GoClaw 想回答的问题是:这套东西能不能用 Go 做得更轻、更稳、更适合长期运行?

项目地址:github.com/smallnest/g…

先说结论:如果你只是想快速做一个 Agent Demo,Python 和 Node.js 依然是更自然的选择;但如果你开始在意部署、稳定性、可观测性和长期运行,那么 Go 其实是一条很值得认真考虑的路。

这篇文章想讲清楚三件事:

  • 为什么我会想用 Go 重做一套 Agent 框架

  • GoClaw 的核心架构到底解决了什么问题

  • 它在真实任务里,是否真的能把事情做完

图片

这是什么?

这两年,大家做 AI Agent,很多时候默认会选 Python 或 Node.js。原因很现实:生态成熟、轮子够多、上手也快。

但如果你真的想把一个 Agent 长期跑起来,问题很快就会从“怎么调模型”变成“怎么部署、怎么运维、怎么保证它别轻易挂掉”。

也正因为这样,我一直在想:如果把 OpenClaw 这套已经被验证过的设计思路,用 Go 重新实现,会变成什么样?

GoClaw 就是在这个背景下做出来的。

图片

它是一个用 Go 编写的 AI Agent 框架,灵感来自 OpenClaw。这里强调“框架”,是因为它不只是一个聊天机器人,而是一套完整的运行体系:能接入消息平台、执行任务、调用工具,也能继续扩展新能力。

如果把 OpenClaw 看作一套成熟的 Agent 设计,那么 GoClaw 更像是一次“用 Go 把它工程化重做”的尝试。这样做的好处很直接:单一二进制部署,编译后就是一个可执行文件,扔到服务器上就能跑;模块边界更清晰,消息通道、工具系统、技能系统、记忆系统彼此解耦;运行时也更稳,重试、故障转移、熔断器这些可靠性机制可以直接内建进去。

它能接哪些平台?Telegram、WhatsApp、飞书、钉钉、微信、企业微信、Slack、Discord、Google Chat、Microsoft Teams、百度如流等常见 IM 与协作平台都已经覆盖。你也可以通过 WebSocket Gateway 对接自己的系统,或者直接使用内置的 Dashboard。

如果只用一句话概括 GoClaw,可以这么说:它想做的不是“再造一个聊天机器人”,而是提供一套适合长期运行、方便扩展、并且足够稳的 Go 版 Agent 基础设施。

换句话说,GoClaw 想解决的不是“模型能不能更聪明”,而是“一个 Agent 系统能不能真正跑起来、跑得久、出了问题还能查”。

核心架构:一条看似简单,其实很难跑稳的链路

下面这张图是OpenClaw的架构图,GoClaw也是参考这个架构实现的,尽管在一些细节上有一些区别,但是整体架构是一样的:

图片

从外面看,GoClaw 的工作流并不复杂:消息进来,Agent 处理,结果再发出去。

真正难的地方不在这条主链路本身,而在主链路背后那一堆容易被忽略的问题:状态怎么管理?工具调用失败了怎么办?用户中途插话怎么打断?上下文太长了怎么恢复?账号限流了怎么切备用?

这些问题不解决,Agent 看起来能跑,实际上并不适合长期运行。

系统怎么运转?

用户消息先到 Channel 适配器。适配器负责把不同平台的格式转成统一的内部消息格式,然后扔到 MessageBus。Agent 从 Bus 拿消息,找到或创建对应的 Session,调用 Orchestrator 执行。

Orchestrator 是核心协调器,它管理 LLM 调用、工具执行、状态维护。下面有 AgentState 管消息历史和队列,ContextBuilder 组装系统提示词,RetryManager 处理重试和故障转移。工具系统通过 ToolRegistry 注册,技能系统通过 SkillsLoader 加载。

Provider 层负责对接 LLM。支持 OpenAI、Anthropic、OpenRouter 等提供商,还支持配置轮换和故障转移——主账号挂了自动切备用账号。

双循环机制:为什么很多 Agent 看起来能跑,其实一复杂就散?

GoClaw 用双循环处理消息。原因很简单:很多真实任务都不是“一次 LLM 调用”就能结束的。

很多 Demo 能跑,是因为它只处理“一问一答”。但只要任务一复杂,比如要查信息、调用工具、等待结果、接着继续判断,你就会发现单轮执行很快不够用了。

外层循环处理 FollowUp 消息。所谓 FollowUp,可以理解为“这件事做完以后,下一步还要继续做什么”。比如用户说“帮我查下明天天气,如果下雨就提醒我带伞”,Agent 查完天气后,会继续判断是否需要发提醒,这就是一个典型的 FollowUp 任务链。

内层循环处理工具调用和 Steering。工具调用比较直观: LLM 先决定“要读文件”或“要执行命令”,系统把结果拿回来,再继续下一轮。Steering 则是中断机制。用户在 Agent 执行过程中突然插一句“停下,别干了”,这条消息应该优先级更高,能够立即打断当前工具链,转去处理紧急指令。

用户消息 → Channel 适配器 → MessageBus → Agent.handleInboundMessage()
                                        │
                                        ▼
                               Session 获取/创建
                                        │
                                        ▼
                               Orchestrator.Run()
                                        │
                    ┌───────────────────┴───────────────────┐
                    │                                       │
                    ▼                                       ▼
            ┌───────────────────┐                  ┌───────────────┐
            │    外层循环        │                  │ 检查 FollowUp  │
            │ (FollowUp 处理)    │◄──────────────── │   消息队列     │
            └─────────┬─────────┘                  └───────────────┘
                      │                                   ▲
                      ▼                                   │
            ┌───────────────────┐                         │
            │    内层循环        │                         │
            │ (工具调用处理)      │                         │
            │                   │                         │
            │  LLM调用 → 工具执行 │                         │
            │         ↓         │                         │
            │  检查 Steering     │─── 有 ──► 中断 ──────────┤
            │         ↓ 无      │                          │
            │  继续工具链         │                         │
            └─────────┬─────────┘                         │
                      │                                   │
                      ▼                                   │
            内层循环结束 ───────────────────────────────────┘
                                        │
                                        ▼
            结果处理 → Session 更新 → Bus 发布 → Channel 发送

这和 OpenClaw 的设计有什么不同?两者都采用双循环,但恢复逻辑放置的位置不同。OpenClaw 更倾向于把重试、故障转移和上下文压缩放在外层;GoClaw 则把这些恢复逻辑更多收敛到内层执行路径里,外层循环主要负责 FollowUp。前者更容易把整体流程看清楚,后者则更利于把失败恢复和工具执行放在同一个语义闭环里。

核心组件:真正让系统转起来的,不只是模型

图片

Agent 和 Orchestrator

Agent 是主代理类,负责管理消息处理和生命周期。它更像一个总入口,不直接执行业务细节,而是把请求分发给各个子系统。Orchestrator 则更接近“执行中枢”,负责 LLM 调用循环、工具执行和中断处理。

如果把 GoClaw 类比成一个小型操作系统,那么 Agent 像入口层,Orchestrator 像调度层,AgentState 则像运行时状态。

// Agent 的核心字段(省略锁、订阅等辅助字段)
type Agent struct {
    orchestrator       *Orchestrator
    bus                *bus.MessageBus
    provider           providers.Provider
    sessionMgr         *session.Manager
    tools              *ToolRegistry
    context            *ContextBuilder
    workspace          string
    skillsLoader       *SkillsLoader
    helper             *AgentHelper
    maxHistoryMessages int
    state              *AgentState
}
// Orchestrator 的核心字段
type Orchestrator struct {
    config     *LoopConfig
    state      *AgentState // 作为模板的初始状态
    eventChan  chan *Event
    cancelFunc context.CancelFunc
}

AgentState:为什么很多 Agent 一复杂就“失忆”?

AgentState 管理整个执行过程中的状态。除了消息历史和系统提示词,它还维护流式输出状态、待处理工具、Steering 队列和 FollowUp 队列,是 Orchestrator 每轮执行都会反复读写的核心对象。

很多 Agent 的不稳定,本质上不是模型不够强,而是状态管理太松散。状态一散,恢复、续跑、中断、调试都会变得很痛苦。

type AgentState struct {
    SystemPrompt  string
    Model         string
    Provider      string
    ThinkingLevel string
    Tools         []Tool
    Messages      []AgentMessage
    IsStreaming   bool
    StreamMessage *AgentMessage
    PendingTools  map[string]bool
    Error         error
    SteeringQueue []AgentMessage
    SteeringMode  MessageQueueMode
    FollowUpQueue []AgentMessage
    FollowUpMode  MessageQueueMode
    SessionKey   string
    LoadedSkills []string
}

Steering 和 FollowUp:一个负责打断,一个负责继续

Steering 是中断式消息。用户在 Agent 执行过程中说"停",这条消息会立即插入到当前对话,打断正在执行的工具链。适用于紧急停止、修改指令等场景。

agent.Steer(AgentMessage{
    Role: RoleUser,
    Content: []ContentBlock{TextContent{Text: "紧急停止"}},
})

FollowUp 是后续消息。Agent 完成当前任务后,自动处理这些排队的消息。适用于任务链、异步回调、多步骤流程。

agent.FollowUp(AgentMessage{
    Role: RoleUser,
    Content: []ContentBlock{TextContent{Text: "继续下一个任务"}},
})

ContextBuilder:系统提示词不是写死的

ContextBuilder 负责动态组装系统提示词。它不是把一大段固定 Prompt 硬塞给模型,而是根据运行场景选择不同的上下文深度。full 模式用于主 Agent,包含身份、工具、技能、CLI 参考等完整信息;minimal 模式主要给子 Agent 使用;none 模式则只保留最基本的身份信息。

这里还涉及到上下文窗口的压缩问题。我在实现 GoClaw 时踩过一个很典型的坑:一开始我用的是一种比较粗暴的压缩策略,只保留最近的 n 条消息,把更早的历史改写成摘要塞回上下文。这个办法看起来简单,但消息之间其实是有关联的,尤其一旦涉及 tool 调用,前后消息关系不能随便截断。此前那种过于粗暴的实现,就曾导致压缩后消息对应关系错位,进而影响后续回复的正确性。

重试机制:真正的工程问题,从失败那一刻才开始

在 Agent 系统里,重试如果只是简单地“再来一次”,往往意义不大。GoClaw 的重试逻辑同时带着恢复策略:遇到上下文溢出,可以压缩历史消息;遇到账号问题,可以轮换到备用账号;遇到临时故障,可以指数退避后再试。

这也是 GoClaw 和很多“能跑起来的 Demo”之间的差别:前者把失败当成设计对象,后者通常只把成功路径写通。

type RetryConfig struct {
    MaxAttempts        int           // 最大重试次数
    BaseDelay          time.Duration // 基础延迟
    MaxDelay           time.Duration // 最大延迟
    ProfileRotation    bool          // 配置轮换
    ContextCompression bool          // 上下文压缩
}

工具系统:光会说不够,Agent 还得真的能做事

图片

工具是 Agent 的”手脚”。LLM 负责判断和生成计划,真正去读文件、跑命令、抓网页、发消息,靠的是工具层。GoClaw 选择的是一套偏通用、可组合的核心工具,而不是针对每个场景都去发明一个专用工具。

这种设计背后的取舍很明确:工具尽量少,但每个工具都足够通用。这样系统核心不会迅速膨胀,复杂场景则交给技能系统去补。

常见的工具大致分几类:文件操作有 read_filewrite_filelist_files;命令执行有 run_shellprocess;网络相关有基于 Chrome DevTools Protocol 的 browser_*web_searchweb_fetch;另外还有 use_skillmessagecronsession_status 这些偏系统能力的工具。

工具接口本身并不复杂:声明名称、描述、参数 Schema,再实现执行逻辑即可。为了支持 UI 展示和流式回传,接口里还额外定义了 Label() 和带 onUpdate 回调的 Execute()

type Tool interface {
    Name() string
    Description() string
    Parameters() map[string]any
    Label() string
    Execute(ctx context.Context, params map[string]any, onUpdate func(ToolResult)) (ToolResult, error)
}

技能系统:为什么我更想写 Markdown,而不是继续写插件

图片

技能系统是 GoClaw 一个很有代表性的设计。Skill 更接近”知识模块”,而不是传统意义上的代码插件。

为什么这么设计?因为写代码插件的门槛高,写 Markdown 文档的门槛低得多。技能通过 Prompt Injection 实现:系统读取 SKILL.md,提取元数据和正文,再把它注入系统提示词里,LLM 就知道在什么场景下应该怎么做。

这件事的价值不只是“更方便扩展”,更重要的是它把扩展能力从“写代码的人”手里,部分转移到了“懂业务的人”手里。

一个技能文件长什么样?开头是 YAML 格式的元数据,定义名称、描述、依赖等。后面是 Markdown 格式的内容,告诉 LLM 在什么情况下该做什么。

---
name: weather
description: Get current weather and forecasts via CLI.
metadata:
  openclaw:
    emoji: "🌤️"
    requires:
      bins: ["curl"]
      pythonPkgs: ["requests"]
---
# Weather Forecast
When the user asks about weather:
1. Use `run_shell` to execute: `curl wttr.in/{city}?format=3`
2. Parse the output and present it to the user

可以看到这个技能中额外补充了metadata数据,可以更好的告诉智能体如何使用这个技能,包括技能的安装,根据当前环境对技能进行筛选等。

技能加载流程大致是这样:先扫描技能目录,再解析 SKILL.md,提取元数据和正文;然后检查依赖,看所需的二进制、环境变量、Python/Node 包是否满足;最后采用两阶段注入,先把技能摘要告诉 LLM,再在它调用 use_skill 之后注入完整内容。这样既能控制提示词体积,也能减少无关技能对当前任务的干扰。

多通道支持:Agent 如果进不了消息通道,就永远只是个 Demo

图片

GoClaw 能接入多种消息平台。每个平台在内部都对应一个 Channel,实现统一接口。Channel 的职责并不复杂:接收消息、转换格式、发送回复。但正是这层抽象,让 Agent 可以同时跑在多个平台上,而不是被某一个 IM 生态绑死。

说得直接一点,Agent 如果进不了真实消息通道,就很容易永远停留在“本地命令行里挺好用”的阶段。

目前已经支持的平台包括:Telegram(Bot 模式)、WhatsApp(Business API)、飞书(机器人)、钉钉(Stream 模式)、微信(个人号扫码登录)、企业微信(机器人)、Slack(Bot)、Discord(Bot)、Google Chat(Bot)、Microsoft Teams(Bot)、百度如流(企业通讯)以及 Gotify(推送)。

微信通道比较特殊,基于腾讯 OpenClaw-weixin 插件协议实现。首次使用需要先扫码登录,完成后才能正常收发消息。

# 扫码登录
goclaw channels weixin login my-weixin
# 查看状态
goclaw channels weixin status my-weixin
# 登出
goclaw channels weixin logout my-weixin

而且这个微信插件的实现也非常的简单。当微信官方开始灰度OpenClaw的插件的时候,很多开发者都尝试把这个功能接入到其他智能体中。我安装了这个插件后,我就给Claude Code一句话『参考 @tencent-weixin/openclaw-weixin-cli的实现,为goclaw增加微信channel的支持』,Claude Code直接就给我生成了相应的代码。

Gateway 和 Dashboard:一个能长期跑的系统,不能只有聊天窗口

图片

WebSocket Gateway 是一个独立服务,提供 WebSocket 和 HTTP 接口。其他系统可以通过 Gateway 调用 Agent,因此它既可以作为远程入口,也可以作为多端接入层。

这层的意义在于,它把 GoClaw 从“一个本地进程”升级成了“一个可以被其他系统调用的服务”。

# 启动 Gateway
# 更简化的临时运行命令是 goclaw start
goclaw gateway run
# 自定义端口
goclaw gateway run --port 8080 --bind 0.0.0.0
# 安装为系统服务
goclaw gateway install
goclaw gateway start

Dashboard 则是内置的 Web 界面,提供实时聊天、会话管理、Channel 状态监控、Cron 任务管理、RPC API 调用等能力。启动 Gateway 后,访问 http://localhost:28789/dashboard/ 就可以直接使用。

Cron 调度系统:真正的助手,应该会自己动起来

图片

如果一个 Agent 只能被动等你发消息,那它更像聊天机器人;只有具备定时执行和后台触发能力,它才更像”长期运行的助手”。GoClaw 内置了定时任务调度器,支持固定时间、固定间隔和 Cron 表达式三种调度方式。

日报、巡检、定时提醒、定时抓取、周期性健康检查,这些都属于“Agent 从对话走向系统”的关键一步。

# 定时执行(每天 14:30)
goclaw cron add --name "Daily Report" --at "14:30" --message "生成日报"
# 间隔执行(每小时)
goclaw cron add --name "Hourly Check" --every "1h" --system-event "health_check"
# Cron 表达式
goclaw cron add --name "Weekly Backup" --cron "0 2 * * 0" --message "执行备份"
# 立即运行
goclaw cron run job-123
# 查看历史
goclaw cron runs --id job-123

当然更好的方式是在聊天对话框中,使用平常的语言设置定时任务即可。上面的命令行工具是参考OpenClaw实现的命令行管理工具,我们并不常用。

记忆系统:没有记忆的 Agent,本质上只是一次性会话

图片

一个真正可用的 Agent,不应该每次开口都像”失忆”。GoClaw 的记忆系统大致可以分成三层:第一层是会话记录,也就是本地持久化的 JSONL 对话历史;第二层是向量记忆,用于做语义检索;第三层是 QMD(Quick Markdown Database),用 Markdown 组织长期知识,适合沉淀结构化笔记和长期记忆。

这三层叠在一起,才比较接近“能持续协作”的 Agent,而不是“每次都要重新介绍背景”的聊天模型。下面这个示例也更接近 GoClaw 当前真实使用的配置结构:

{
  "memory": {
    "backend": "builtin",
    "builtin": {
      "enabled": true,
      "database_path": "",
      "auto_index": true
    },
    "qmd": {
      "command": "qmd",
      "enabled": false,
      "include_default": true,
      "paths": [
        {
          "name": "notes",
          "path": "~/notes",
          "pattern": "**/*.md"
        }
      ],
      "sessions": {
        "enabled": false,
        "export_dir": "~/.goclaw/sessions/export",
        "retention_days": 30
      }
    }
  }
}

安全机制:Agent 越像助手,就越不能忽视安全

图片

AI Agent 一旦能读文件、跑命令、访问网络,安全就不是一个附加项,而是基础前提。GoClaw 在这件事上的思路,是把风险拆成多层,再分别处理。

很多人聊 Agent,容易把注意力都放在“能不能更强”;但真正进入生产环境之后,优先级往往会立刻反过来,先问“会不会出事”。

在当前配置体系里,最直接的一层安全控制还是工具级限制,比如 shell 开关、危险命令黑名单、超时、工作目录,以及浏览器和 Web 工具的启用状态。这些配置不花哨,但非常实用。

{
  "tools": {
    "shell": {
      "enabled": true,
      "allowed_cmds": [],
      "denied_cmds": ["rm -rf", "dd", "mkfs"],
      "timeout": 30,
      "working_dir": ""
    },
    "web": {
      "search_api_key": "",
      "search_engine": "travily",
      "timeout": 10
    },
    "browser": {
      "enabled": true,
      "headless": true,
      "timeout": 30
    },
    "cron": {
      "enabled": true,
      "store_path": "~/.goclaw/cron/jobs.json"
    }
  }
}

命令过滤是另一层防护。黑名单 denied_cmds 阻止危险命令执行,白名单 allowed_cmds 只允许特定命令执行。还会阻止危险的 shell 构造,比如命令替换、重定向、子 shell 等。

LLM 提供商:别把整个系统的命门,交给一个模型或一个账号

图片

在工程实践里,模型能力当然重要,但更重要的是不要把整个系统绑死在单一模型或单一账号上。GoClaw 目前主要支持四类 provider:OpenAI、Qianfan(百度千帆,走 OpenAI-compatible 接口)、Anthropic,以及 OpenRouter。像 GPT-4、GPT-4o、DeepSeek 这类模型,只要底层接口兼容,也都可以接进来。

因为一旦系统真的跑起来,限流、欠费、波动、服务异常都不是“小概率事件”,而是迟早会遇到的日常。

从当前配置结构看,提供商配置是按 provider 展开定义的,模型则放在 agents.defaults.model 中引用。推荐直接使用显式前缀,把 provider 和模型绑清楚,比如 qianfan:deepseek-v3.2openai:gpt-4oanthropic:claude-3-5-sonnet。下面这个示例更贴近 GoClaw 现在实际使用的配置方式:

{
  "agents": {
    "defaults": {
      "model": "qianfan:deepseek-v3.2",
      "max_iterations": 15,
      "temperature": 0.7,
      "max_tokens": 4096
    }
  },
  "providers": {
    "qianfan": {
      "api_key": "YOUR_QIANFAN_API_KEY",
      "base_url": "https://qianfan.baidubce.com/v2",
      "timeout": 600
    },
    "openai": {
      "api_key": "YOUR_OPENAI_API_KEY",
      "base_url": "",
      "timeout": 600
    },
    "openrouter": {
      "api_key": "YOUR_OPENROUTER_API_KEY",
      "base_url": "",
      "timeout": 600,
      "max_retries": 3
    },
    "anthropic": {
      "api_key": "YOUR_ANTHROPIC_API_KEY",
      "base_url": "",
      "timeout": 600
    }
  }
}

项目结构:代码是怎么组织起来的

图片

代码组织按功能模块划分。agent/ 是核心逻辑,包括 Agent 主类、Orchestrator 协调器、ContextBuilder、RetryManager、工具注册表、技能加载器等。channels/ 是各种消息通道实现。bus/ 是消息总线。providers/ 是 LLM 提供商对接。session/ 是会话管理。memory/ 是记忆系统。gateway/ 是 WebSocket 网关。cron/ 是定时任务。cli/ 是命令行界面。config/ 是配置管理。

goclaw/
├── agent/                    # Agent 核心逻辑
│   ├── agent.go             # Agent 主类
│   ├── orchestrator.go      # 执行协调器
│   ├── context.go           # 上下文构建器
│   ├── retry.go             # 重试机制
│   ├── types.go             # 类型定义
│   └── tools/               # 工具实现
├── channels/                 # 消息通道
├── bus/                      # 消息总线
├── providers/                # LLM 提供商
├── session/                  # 会话管理
├── memory/                   # 记忆系统
├── gateway/                  # WebSocket 网关
├── cron/                     # 定时任务
├── cli/                      # 命令行界面
├── config/                   # 配置管理
└── internal/                 # 内部包

快速开始:先跑起来,再慢慢扩展

图片

如果你只是想尽快把 GoClaw 跑起来,路径其实很直接:克隆仓库,安装依赖,编译二进制,写最小配置,然后启动。

# 克隆仓库
git clone https://github.com/smallnest/goclaw.git
cd goclaw
# 安装依赖
go mod tidy
# 构建
go build -o goclaw .
# 或完整构建(包含 UI)
make build-full
# 创建配置
cat > ~/.goclaw/config.json << EOF
{
  "workspace": {
    "path": ""
  },
  "agents": {
    "defaults": {
      "model": "qianfan:deepseek-v3",
      "max_iterations": 15,
      "temperature": 0.7,
      "max_tokens": 4096
    },
    "runtime": {
      "type": "claude-code",
      "claude_code": {
        "command": "/path/to/claude"
      }
    }
  },
  "providers": {
    "qianfan": {
      "api_key": "YOUR_QIANFAN_API_KEY",
      "base_url": "https://qianfan.baidubce.com/v2",
      "timeout": 600
    },
    "openai": {
      "api_key": "",
      "base_url": "",
      "timeout": 600
    },
    "anthropic": {
      "api_key": "",
      "base_url": "",
      "timeout": 600
    }
  },
  "gateway": {
    "host": "localhost",
    "port": 8080,
    "read_timeout": 30,
    "write_timeout": 30,
    "websocket": {
      "host": "localhost",
      "port": 28789,
      "path": "/ws",
      "enable_auth": false,
      "auth_token": ""
    }
  }
}
EOF
# 启动
./goclaw start
# 或启动 TUI
./goclaw tui
# 或启动 Gateway(含 Dashboard)
./goclaw gateway run
# 访问 http://localhost:28789/dashboard/

设计原则:GoClaw 为什么会长成现在这个样子

图片

GoClaw 的设计大致遵循几条原则。

  • 极简核心,可扩展技能:核心工具保持克制,把扩展能力交给技能系统,避免核心不断膨胀。

  • 串行默认,显式并行:消息处理默认串行,尽量减少竞态;真的需要并行时,再显式引入。

  • 可靠性优于复杂性:重试、故障转移、熔断器这些能力不是“锦上添花”,而是默认配置。

  • 完全可观测:结构化日志、会话持久化、任务轨迹都尽量保留,出了问题要能查。

  • 遵循 Go 的工程习惯:Context 传播、Channel 通信、接口组合,不强行套一层不必要的抽象。

如果把这篇文章压缩成一句结论,那就是:GoClaw 并不是想在 Agent 世界里发明一种全新的范式,而是想把一套已经被验证过的设计,用 Go 的方式做得更稳、更轻、更容易落地。

如果你也在做 Agent,而且已经开始从“怎么把 Demo 跑起来”,转向“怎么把系统长期跑下去”,那么 GoClaw 也许正是一个值得参考的方向。

一个真实实践:让 GoClaw 帮我把磁盘扩容这件事做完

图片

讲架构、讲设计原则,终究还是偏”怎么想”;真正能说明一个 Agent 框架有没有价值的,往往是”它能不能把事做完”。

最近我在养goclaw过程中就遇到了一个很典型的例子:扩展OpenClaw服务器磁盘空间。

事情的起因很简单。我一直对磁盘空间比较敏感,平时会让自己的 Agent 去检查机器磁盘使用情况。有一次检查时,它给出的结果和我的印象对不上:这台机器买的时候明明是 128GB,但系统里实际可用的分区却只有六十多 GB。

继续排查之后,问题就清楚了:原来机器上还有大约 58GB 的空间根本没有分配到当前分区,这是我在安装Ubuntu的时候遗漏了。这不是常规的“磁盘快满了,该清理了”,而是一个更偏系统运维的问题:先确认现状,再识别未分配空间,最后决定怎么把它扩容到根分区。

这个场景很适合检验 GoClaw 这种 Agent 框架到底有没有实战价值,因为它不只是回答一个问题,而是要完成一整条链路:

  • 先检查磁盘和分区状态

  • 再分析问题到底出在“磁盘占满”还是“空间未分配”

  • 然后给出可执行方案

  • 最后在确认之后真正执行操作

图片

图片

图片

我只说了"立即"两个字,它就帮我完成了:

从框架视角看,这件事刚好把 GoClaw 的几层能力串了起来。

  • 工具系统负责执行实际命令,读取分区信息,完成磁盘检查和后续操作。

  • AgentState 和会话上下文负责保留中间判断,避免做到一半“失忆”。

  • 双循环机制让它可以先完成检查,再进入下一步执行,而不是一次回答就结束。

  • 安全机制则提醒你:这类系统级操作最好只在可控环境中进行,不要一上来就在关键机器上放开权限。

图片

更重要的是,这个案例说明了 GoClaw 适合做的不是“陪你聊聊天”,而是“替你把一个你知道目标、但不想自己手敲每一步命令的任务跑完”。

当然,这类能力也天然伴随着边界。像磁盘扩容、文件删除、系统配置修改这种操作,更适合先在实验机、测试环境或者你完全可控的机器上使用。Agent 能帮你提高效率,但前提仍然是:权限要可控,风险要可知,回滚路径要提前想清楚。

如果说前面那些章节解释的是 GoClaw 的设计思路,那么这个实践案例展示的就是另一件更实际的事:当 Agent 真正接进系统、工具和运行环境之后,它开始从“能回答问题”变成“能替你完成任务”。

打造高效易用的Agent Skill

作者 百度Geek说
2026年3月11日 14:40

导读 introduction

Agent 能写代码、能调工具,但它不了解你团队的规范、流程和质量标准,每次对话都从零教起,既低效又不稳定。Skill 机制正是为解决这个问题而生:把你的经验和流程结构化地交给 Agent,让它像拿到工作手册一样自主执行。本文从设计原理、编写方法到评测迭代,梳理 Skill 的实践路径,帮助开发者打造高效易用的Agent Skill。

01 Skill 是什么,为什么需要它

1.1 Agent 的先天缺陷

大模型很聪明,但它有一个根本问题:没有你的私域知识和专属能力

你团队的代码规范是什么?做 Code Review 要看哪几个维度?创建一份 PPTX 应该遵循什么品牌样式?这些东西不在训练数据里,每次对话都重新教一遍既低效又不稳定。

更现实的问题是,即使你通过 MCP 给了 Agent 工具调用能力,能读 GitHub、能查 Sentry、能操作 Linear,它依然不知道该按什么流程、什么顺序、什么标准去使用这些工具。而 Skill 就可以提供这些信息,帮助Agent更好地执行任务。

1.2 从 MCP 到 Skill:能力扩展的演进

Agent 能力扩展的路径,经历了几个关键节点:

MCP(Model Context Protocol) 解决了"连接"问题。2024 年 11 月 Anthropic 开源 MCP,让 Agent 能够标准化地调用外部工具和数据源。这是基础设施层面的突破,Agent 终于能"伸手"触达外部世界了。

AGENTS.md 是社区自发的探索。随着 Cursor、Claude Code 等 AI 编码助手的普及,开发者很快意识到一个问题:这些 Agent 能写代码,但不了解项目的技术栈选择、代码风格约定、架构决策背景。于是社区开始在仓库根目录放置 AGENTS.md,用自然语言把项目的上下文和规范写给 Agent 看。

Skill 则是 Anthropic 在 2025 年 10 月正式推出的标准化方案。它把 AGENTS.md 的理念系统化,不仅仅是一个 Markdown 文件,而是一个结构化的文件夹,包含指令、脚本、参考文档和资源文件,形成完整的知识包。随后,Cursor、Windsurf 等产品也纷纷推出类似机制,Skill 正在成为 Agent 能力扩展的主流范式。

1.3 Skill 的核心设计:渐进式披露

Skill 最精妙的设计在于它的三级渐进式披露(Progressive Disclosure)机制,不会一次性把内容全塞给模型,而是分层按需加载:

第一级:YAML frontmatter 中的 description 字段。 本质上是一段结构化的自然语言声明,包含三层信息:这个 Skill 干什么用(“分析 Figma 设计稿并生成开发交付文档”)、核心能力是什么(“设计规范提取、组件文档生成、标注导出”)、什么时候触发(“当用户上传 .fig 文件或要求’设计转代码交付’时”)。它始终存在于 Agent 的系统提示词中,作用类似索引,当用户输入到来时,Agent 拿请求和所有 Skill 的 description 做匹配,命中了才加载对应 Skill 的完整内容。这个设计意味着你可以同时挂载几十个 Skill,而激活判断的成本只是几十行短文本的比对,不需要把所有 Skill 的完整指令都塞进上下文。

第二级: SKILL.md 正文。 当 Agent 判断某个 Skill 与当前任务相关时,才会读取 SKILL.md 的完整内容。这里包含核心指令、工作流程和关键示例。

第三级: references/  scripts/ references/ 目录下的详细文档、scripts/ 下的可执行脚本,这些只在 Agent 执行过程中确实需要时才会去查阅或调用。

为什么要这么设计?它解决了两个实际问题:

  1. Token 效率:不把所有知识一股脑塞进上下文,避免信息过载。
  2. 注意力聚焦:模型的注意力机制在上下文越长时衰减越明显,渐进式披露让模型在每个阶段只关注最相关的信息。

1.4 怎么组织和安装 Skill

当 Skill 越写越多,散落在各处很快就会失控。推荐一开始就用Git仓库统一管理。

team-skills/
├── code-review/
│   └── SKILL.md
├── react-state-management/
│   ├── SKILL.md
│   └── references/
├── sprint-planning/
│   ├── SKILL.md
│   └── scripts/
└── ...

好处很直接:版本有记录,团队能协作,跨仓库安装迅速。

安装到具体的 Agent 平台时,各家的路径约定不同,但社区已经有了统一的解决方案,Vercel 开源的 skills CLI 工具,一条命令兼容多平台:

# 从 GitHub 安装,自动识别当前环境并放到正确的位置
npx skills add https://github.com/your-team/skills/tree/main/code-review
# 支持 Claude Code、Cursor、Windsurf 等主流 Agent 平台
# 无需关心各平台的路径差异

当然,你也可以手动放置安装。因平台和场景而异路径约定不同,以Claude Code为例:

Claude Code:

# 项目级(只在当前项目生效)
.claude/skills/code-review/SKILL.md
# 全局级(所有项目生效)
~/.claude/skills/code-review/SKILL.md

社区实践一瞥

Skill 的生态正在快速成长。Anthropic 官方提供了一批高质量 Skill, 在anthropics/skills 仓库,尤其是 pdfskill-creatorfrontend-design 这几个,它们很好地展示了渐进式披露和脚本自动化的最佳实践。这些 Skill 本身就是很好的学习范本。

社区层面,Asana、Atlassian、Figma、Sentry、Zapier 等厂商已经为自己的 MCP Server 配套了 Skill。独立开发者也在持续贡献,从前端设计到代码审查,从数据分析到项目管理,可用的 Skill 库正在不断扩大。

02 如何编写一个 Skill

2.1 基本格式

一个 Skill 在文件系统中是一个文件夹,最小结构只需要一个文件:

your-skill-name/
├── SKILL.md          # 必须,入口文件
├── scripts/          # 可选,可执行脚本
├── references/       # 可选,参考文档
└── assets/           # 可选,模板、图标等资源

命名规则简单但严格:

  • 文件夹名用 kebab-casemy-cool-skill 是正确的,而My Cool Skill 以及my_cool_skill 等都是无效的。
  • 入口文件必须精确命名为 SKILL.md,大小写敏感,skill.md 或 SKILL.MD 都不行
  • 不要在Skill文件夹内放README.md(所有文档放在SKILL.md或 references/ 中)

SKILL.md 的结构分两部分:YAML Frontmatter 和 Markdown 正文

---
name: my-skill-name
description: 做什么。在用户说"XXX"时使用。核心能力包括 A、B、C。
---
# My Skill Name
## Instructions
具体的指令内容...

Frontmatter 用 --- 包裹,其中 name 和 description 是必填字段。正文用标准 Markdown 编写,包含 Agent 执行任务时需要遵循的具体指令。

2.2 工作原理

理解 Skill 的工作原理,有助于写出更有效的 Skill。核心流程是这样的:

阶段一:常驻索引。 你安装的所有 Skill 的 description 字段会被注入到 Agent 的系统提示词中。Agent 在每次对话开始时就"知道"自己拥有哪些 Skill,但不知道具体内容。

阶段二:激活读取。 当用户的请求与某个 Skill 的 description 匹配时,Agent 会使用内置工具(如 view 或 read 命令)读取该 Skill 的 SKILL.md 完整内容。这一步对应 messages[] 中的一个工具调用。

阶段三:执行与深入。 Agent 根据 SKILL.md 中的指令开始执行任务。如果指令中引用了 references/ 下的文档或 scripts/ 下的脚本,Agent 会在需要时再去读取或执行它们。

用 API 的 messages[] 视角来看,一个典型的 Skill 调用大约是这样的

用户消息 → Agent 识别需要 Skill → [工具调用: 读取 SKILL.md] 
→ Agent 获得指令 → [工具调用: 执行任务步骤] → 返回结果

这意味着 Skill 的激活本身会消耗 1-2 步工具调用。所以 description 写得准不准,直接影响 Token 消耗和响应速度,误触发意味着浪费,漏触发意味着能力缺失。

03 编写优质的 Skill

一个 Skill 能不能用和好不好用,差距巨大。这个差距主要体现在两个地方:Description 决定"什么时候用",Body 决定"用起来效果如何"。

3.1 Description:激活的精准度

Description 是整个 Skill 体系中最关键的一行文字。它决定了 Agent 在什么场景下会加载你的 Skill,写得不好,要么该用的时候不触发(under-triggering),要么不该用的时候乱触发(over-triggering)。

三大要素: 一个好的 Description 需要同时回答三个问题

  1. 能做什么:这个 Skill 的核心价值是什么
  2. 核心能力:具体包含哪些能力
  3. 激活条件:用户说什么话、做什么操作时应该触发

正面案例:

# 清晰、具体、包含触发短语
description: >
  分析 Figma 设计稿并生成开发交付文档。当用户上传 .fig 文件、
  要求"设计规范""组件文档""设计转代码交付"时使用。
# 明确的服务边界和触发词
description: >
  管理 Linear 项目工作流,包括迭代规划、任务创建和状态跟踪。
  当用户提到"迭代""Linear 任务""项目规划"或要求
  "创建工单"时使用。

反面案例:

# 太模糊,几乎什么都能匹配
description: Helps with projects.
# 缺少触发条件,Agent 不知道什么时候该用
description: Creates sophisticated multi-page documentation systems.
# 过于技术化,没有用户视角的触发词
description: Implements the Project entity model with hierarchical relationships.

防止过度触发的技巧: 如果你的 Skill 经常在不相关的场景被加载,可以在 Description 中加入"负向触发"说明:

description: >
  CSV 文件的高级数据分析,包括统计建模、回归分析、聚类。
  不要用于简单的数据浏览(那个用 data-viz skill)。

3.2 Body:执行的效果

Description 写好了只是让 Skill 在对的时间出现,Body 的质量才决定最终效果。根据使用场景,Body 通常呈现两种形态:

形态一:知识文档型

适用于需要 Agent 掌握特定领域知识或遵循特定标准的场景。

核心要素:

  • 领域知识:把你的专业判断和决策逻辑写成 Agent 可以理解的规则
  • 质量检查清单:明确定义"什么算做好了",让 Agent 在交付前自查
  • Few-Shot 示例:给出 2-3 个输入输出的范例,比抽象描述有效得多
## Code Review Standards
### Critical Checks (must pass)
1. No hardcoded credentials or API keys
2. All user inputs sanitized
3. Error boundaries on async operations
### Quality Checks (should pass)
1. Functions under 50 lines
2. Meaningful variable names (no single letters except loop counters)
3. Comments explain "why", not "what"
### Example Review
**Input:** A React component with inline styles and no error handling
**Expected output:**
- Flag: inline styles → suggest CSS modules or Tailwind
- Flag: missing error boundary → provide template
- Pass: component size reasonable
- Suggestion: extract magic numbers to constants

形态二:工作流型

适用于多步骤、有固定流程的任务。

核心要素:

  • 步骤清晰:每一步做什么、调用什么工具、预期输出是什么
  • 步骤间校验:上一步的输出满足条件才进入下一步,而不是盲目往下走
  • 可循环迭代:对质量不达标的输出能回到前面的步骤重做
## Sprint Planning Workflow

Step 1: Gather Context

`Fetch current project status from Linear. Validation: Confirm at least 1 active project returned.

Step 2: Analyze Velocity

Calculate team velocity from last 3 sprints. Validation: Velocity data covers at least 2 complete sprints.

Step 3: Draft Plan

Create task breakdown with estimates. Validation: Total story points ≤ average velocity × 0.85 (buffer).

Step 4: Review & Adjust

Present plan to user. If user requests changes: → Return to Step 3 with modified constraints.

Step 5: Execute`

Create tasks in Linear with labels and assignments. Validation: All tasks created successfully, no API errors.

3.3 进阶技巧:分层与自动化

多层渐进: SKILL.md 只放核心指令和工作流主干。详细的 API 文档、完整的示例库、边缘场景的处理方案,都放到 references/ 目录下,在正文中用明确的路径引用:

Before writing API queries, consult references/api-patterns.md for:
- Rate limiting guidance
- Pagination patterns  
- Error codes and handling

这样既保证 Agent 知道有这些资源可用,又不会在每次激活时都加载全部内容。

脚本自动化: 凡是可以用代码确定性完成的事情,就不要让模型用自然语言"理解"着去做。模型理解自然语言有概率性,但代码执行是确定性的。

官方的 PDF、DOCX、PPTX 等 Skill 大量使用了这个模式,核心的文档生成逻辑封装在 Python 脚本中,SKILL.md 只负责告诉 Agent 什么时候调用哪个脚本、传什么参数。

04 基于评测迭代

写完 Skill 不是终点。Skill 本质上是给概率性系统写的指令,“我觉得写得挺好"和"它确实在各种场景下都表现稳定"之间,往往隔着好几轮迭代的距离。评测不是锦上添花,而是 Skill 开发流程中不可省略的一环。

4.1 核心理念:像对待 Prompt 一样对待 Skill

Skill 的 Description 是系统提示词的一部分,Body 是任务执行时的指令集。这使得 Skill 开发和 Prompt 开发面临相似的挑战,而 Prompt 开发有一个被反复验证的基本事实:你无法靠直觉判断一段指令的好坏,只能靠在真实场景中反复测试来验证

这引出三个关键原则:

原则一:分层评测。 Description 和 Body 解决的是完全不同的问题,前者决定"什么时候用”,后者决定"用起来效果如何"。它们的评测方法、评测标准和迭代策略完全不同,必须分开处理。

原则二:对照实验。 “好不好"是相对概念。一个 Skill 的输出质量,只有和某个基线对比才有意义。这个基线可以是没有 Skill 时的裸跑效果,也可以是上一个版本的 Skill。没有对照组,改进就无从衡量。

原则三:人类参与。 自动化评分能覆盖格式、结构、字段完整性这类客观检查,但 Skill 真正的价值,比如审美判断、业务适配度、专业深度,只有人能评估。评测流程的设计必须让人的判断能高效地注入迭代循环。

4.2 评测 Description:触发的精准度

Description 评测要回答一个简单的问题:Agent 在该用这个 Skill 的时候用了吗?在不该用的时候没用吧?

理解触发机制

在动手测之前,先理解两个关于触发的事实:

事实一:Agent 只在觉得自己搞不定时才找 Skill。 简单的一步操作(比如"读一下这个文件”),即使 Description 完美匹配也可能不触发,因为 Agent 判断自己直接就能完成。这意味着你的测试用例必须足够复杂,不然你测的不是 Description 好不好,而是任务够不够难。

事实二:Agent 天生偏向欠触发(under-triggering)。 Description 要写得主动一点,把边界往外推。比如不只写"分析 Figma 设计稿并生成交付文档",而是追加"当用户提到设计规范、UI 组件文档、设计转代码交付,甚至只是上传了 .fig 文件但没明说要干嘛时,都应该使用"。

还有一个常见错误:把"什么时候该用这个 Skill"的信息写在 Body 里。Body 是触发之后才加载的,写了也没有任何帮助。所有触发相关的信息,必须且只能写在 Description 中。

构建评测集

准备 16-20 条测试 query,分两组:

  • 应触发组(8-10 条) :覆盖不同的表述方式,正式的、口语的、没有明确提到 Skill 名称但显然需要它的
  • 不应触发组(8-10 条) :重点选近似场景,而非明显无关的请求
[
  {
    “query”: “我们团队要移除 less-loader,把 .less 文件全部转成 PostCSS 方案。项目比较大有 200 多个 LESS 文件,有复杂的 mixin 嵌套,用哪种方式风险更低?”,
    “should_trigger”: true
  },
  {
    “query”: “项目已经在用 PostCSS 了,现在想加 postcss-px-to-viewport 做移动端适配,postcss.config.js 不知道怎么写。”,
    “should_trigger”: false
  }
]

构建评测集时最容易踩的坑:

  • 测试 query 太干净。 “请帮我做代码审查"这种教科书式的指令在真实场景中几乎不存在。真人会带上文件路径、个人上下文、前因后果,甚至拼写错误和口语缩写。你的测试 query 越像真人说的话,评测结果越有参考价值。
  • 反例太容易。 “写一个斐波那契函数"作为 CSS 迁移 Skill 的反例毫无价值。最有意义的反例是那些共享了关键词但实际需要别的工具,或者触及了 Skill 的领域但处于一个不该触发的上下文中的 query。这些边界 case 才能真正检验 Description 的区分度。
△ code-review skill的触发测试 △ less-to-postcss skill的触发测试

执行评测

逐条把测试 query 发给 Agent,观察它是否加载了对应的 Skill。记录结果,计算两个指标:

  • 召回率:应触发组中实际触发的比例(衡量"该用的时候用了没”)
  • 精确率:不应触发组中正确未触发的比例(衡量"不该用的时候克制住了没”)

💡 一个快速调试技巧:直接问 Agent “你什么时候会使用 [skill-name] 这个 Skill?”,它会把 Description 复述回来,你可以据此判断它的理解是否与你的意图一致。

迭代改进

根据失败 case 分析原因,调整 Description:

  • 漏触发居多:补充更多触发关键词和场景描述,把边界推得更宽
  • 误触发居多:增加负向说明(“不要用于…”),收窄适用范围
  • 两者都有:Description 可能定位模糊,需要重新理清这个 Skill 的核心边界

每次修改后,用完整评测集重跑,对比前后得分。注意不要只盯着失败的 case 做针对性修补。Description 最终要面对的是无穷多种真实 query,过拟合到几条测试用例没有意义。

4.3 评测 Body:输出质量

Body 的评测比 Description 复杂得多,因为"好不好"不是布尔值,而是一个多维度的质量判断。核心方法是有 Skill 和无 Skill 的对照实验

Step 1:设计测试用例

准备 2-5 个代表性的测试任务。好的测试用例有几个特征:

  • 覆盖 Skill 的核心能力,不要只测边缘功能
  • 有明确的可判断的输出,而不是开放性的问答
  • 复杂度接近真实使用场景,太简单的任务区分不出有无 Skill 的差异

每个测试用例准备好输入材料(需要审查的代码、需要分析的数据、需要处理的文档等)。

Step 2:对照实验

对每个测试用例,分别跑两次:

  • 实验组:正常加载 Skill,执行任务
  • 对照组:不加载 Skill(或加载旧版本 Skill),执行相同任务

关键要求:用相同的 Agent、相同的输入、相同的系统环境。唯一的变量是 Skill 的有无或版本差异。

把输出保存在结构化的目录中,方便后续对比:

eval-workspace/
├── iteration-1/
│   ├── test-case-auth-module/
│   │   ├── with-skill/
│   │   └── baseline/
│   ├── test-case-api-refactor/
│   │   ├── with-skill/
│   │   └── baseline/
│   └── …

Step 3:定义评判标准

在看结果之前(避免结果影响标准),先想清楚"什么算好"。评判标准分两类:

可程序化验证的客观标准,用脚本直接检测:

  • 输出文件格式是否合法(JSON schema 校验、文件是否可打开)
  • 必要字段是否存在
  • 是否满足特定的结构要求

需要人判断的主观标准,形成检查清单:

  • “每个问题是否附带了具体的修改建议,而非仅描述问题”
  • “是否有将正确代码误标为问题的情况”
  • “输出的优先级排序是否合理”

对于写作风格、设计审美这类高度主观的 Skill,不需要勉强定义细粒度标准,直接看输出、做整体判断,反而更有效。

Step 4:评分和对比

逐个翻看每个测试用例的两组输出,记录:

  1. 客观检查项的通过情况:跑脚本,统计通过率
  2. 主观判断和具体反馈:哪里好、哪里差、哪里出乎意料。反馈要写具体。"输出不够好"没有行动指引,“安全维度的审查遗漏了 SQL 注入风险,建议在 Skill 中增加 OWASP Top 10 检查清单"才能指导改进
  3. 效率数据:如果可获取,记录 token 消耗和响应时间,避免质量提升以不可接受的效率代价为前提

最终形成一个清晰的判断:Skill 版本在哪些维度上比基线好、在哪些维度上持平、在哪些维度上退步了

Step 5:分析和改进

基于评分结果和具体反馈,修改 Skill。这一步是整个迭代中最需要判断力的环节,几个关键原则:

从反馈中提炼通用规律,别过拟合到具体用例。 Skill 最终要在无数不同的真实任务上运行,你现在只是用几个测试用例来快速迭代。如果某个改动解决了测试用例 B 的问题但让测试用例 A 退步了,大概率你在做过于针对性的调整。好的改动应该是普适的。

保持指令精简。 如果能获取到 Agent 的执行过程(而不只是最终输出),仔细看看它在做什么。如果 Agent 花了大量步骤在做无用功,找到 Skill 中导致这些无用功的指令,砍掉试试。冗余的指令不只是浪费 token,还会分散模型的注意力,降低真正重要的指令的执行质量。

解释 why 而不是堆 MUST。 如果你发现自己在写 ALWAYS 或 NEVER 这种全大写的硬约束,先停下来想想,能不能换成解释"为什么这件事重要”。模型理解了原因之后,执行的灵活性和准确度通常都比死记硬背的规则好。硬约束应该留给那些真正不可违反的底线,而不是泛滥在每一条指令里。

关注重复劳动。 如果你在多个测试用例的输出中发现 Agent 都独立编写了类似的辅助脚本或做了类似的预处理工作,这说明这个步骤应该被提炼到 Skill 的 scripts/ 目录下直接复用,而不是每次让 Agent 从头造轮子。

常见问题和改进方向参考:

图片

△ body的评测结果 - 有无skill对比 △ body的评测结果 - 经过迭代对检出问题细节优化

4.4 循环迭代

把上面的步骤连成闭环,每一轮迭代的流程是:

  1. 跑对照实验:在新的目录下同时跑所有测试用例的实验组和对照组
  2. 评分:客观指标跑脚本,主观维度人工判断
  3. 分析反馈:哪里好了、哪里退步了、哪里还不够
  4. 改 Skill:基于反馈修改 SKILL.md 或脚本,遵循上述改进原则
  5. 重跑:用完整评测集验证改动效果

对照组的选择取决于你要回答的问题。如果是新建 Skill,对照组就是没有 Skill 的裸跑,你要证明 Skill 的存在有价值。如果是改进已有 Skill,对照组可以是旧版本,你要证明改动带来了正向提升。

终止条件:反馈趋于空白(没什么要改了)、你已经没有更多手段继续改进、或者你对输出质量满意了。不需要追求完美,Skill 和代码一样,可以持续迭代,在实际使用中收集到新的失败 case 时随时回来改进。

4.5 案例:Skill 迭代的实际路径

案例一:Skill-Creator 的三次进化

Anthropic 官方的 Skill-Creator 本身就经历了迭代式演进:

  • 第一版(创建) :帮用户从自然语言描述生成 SKILL.md,输出格式正确的 Frontmatter 和基本指令结构。核心价值是降低上手门槛。
  • 第二版(创建 + 优化) :增加了分析与改进的能力,将自身能力边界进行了拓展,可以承接几乎所有与Skill相关的工作,因此其description也变得更为激进。用户指出Skill执行时的问题和现象后,可以自主改进Skill内容并给出建议。
  • 第三版(自动评测优化) :基于完整的评测改进循环理论进行构建,不仅仅为生成、改进内容工作负责,也为Skill的最终运行效果负责。这一版可以基于需求生成评测用例、创建评分机制、运行评测、评价汇总、循环改进,完成Skill编写的同时给出效果结论。

案例二:Code-Review Skill 的质量提升

一个更贴近业务的例子,代码审查 Skill 的迭代过程:

  • 第一版(简单 Prompt) :一段直白的 Markdown 指令,列出审查维度和注意事项,以及项目隐式需要注意的的点。效果还行,但输出质量波动大,有时遗漏重要问题,有时对细枝末节过度关注,如果git diff的文件信息过多上下文会超出导致失败。
  • 第二版(多 Agent 组合架构) :引入 SubAgent 模式,每个 Subtask Agent 只持有一个文件的diff + 源码,不会被其他文件干扰。单 Agent 串行审查时,随文件数增加上下文污染越来越严重;并发子Agent 则始终保持干净的注意力窗口。把一次 Code Review 拆解为多个阶段,总览分析(掌握全局)、分维度审查(安全、性能、可维护性分别深入)、使用子agent交叉验证(排除误报)、去重合并(消除冗余)、最终报告(按优先级排序输出)。每个阶段有明确的输入输出契约和质量检查点。依赖文件系统,有明确的“任务是否全部完成”的可检查标准,即使因为网络超时中断,也可以恢复继续处理任务,单个子任务失败不影响其他任务的完成,失败的任务重新跑而无需跑整个PR。

两个版本在相同的 20 个 PR 上跑评测,用 Grader Agent 评估输出质量、覆盖率和误报率,第二版在三项指标上均有明显提升。

图片

△ 旧架构的检出效果 △ 新架构的实现效果,更关注逻辑实现和减少误判

05 总结

Skill 正在统一 Agent 能力扩展的途径。 从 MCP 提供工具连接,到 AGENTS.md 的社区探索,再到 Skill 的标准化方案,Agent "学习新技能"的方式正在收敛。渐进式披露的设计不仅节省 Token,更重要的是提升了模型的注意力分配效率。以自然语言为载体的知识表达,比硬编码的逻辑更灵活,也更 Agentic。

广泛的社区 Skill 可以直接提升生成效果。 Anthropic 官方的文档生成 Skill(PDF、DOCX、PPTX)、前端设计 Skill,以及社区贡献的各类工作流 Skill,都可以拿来即用。在你动手定制之前,先看看现有 Skill 能否满足需求。

定制化 Skill 是让 Agent 在你的场景中真正好用的关键投入。 通用的 Agent 能力就像一个聪明但不了解你业务的新人,Skill 就是你给他的工作手册。Description 的精准度决定了它出现在正确的场景,Body 的质量决定了它在场景中的表现。这两者都有明确的设计原则和可遵循的技巧。

评测是 Agentic 工程必不可少的环节。 不只是工具开发、系统开发需要评测,Skill 开发同样需要。拍脑袋觉得"差不多了"和用数据验证"确实好了"之间,往往隔着好几轮迭代的距离。基于评测的循环优化,评测、分析、改进、重新评测,是通往高质量 Skill 的可靠路径。

回过头看,Skill 做的事情并不复杂:把你本来每次都要重新交代的经验、流程和标准,整理一次存下来,之后 Agent 自己就知道该怎么做了。省掉重复劳动,换来稳定可预期的输出。

基于Spark的配置化离线反作弊系统

作者 百度Geek说
2026年3月5日 16:17

导读 introduction

在作弊手段日益隐蔽和复杂的背景下,单纯依赖在线或实时风控已难以满足深度治理需求。本文系统介绍了一套基于 Spark 的配置化离线反作弊挖掘框架,重点解析其 Extract、Accumulate、Join、Policy 四大核心模块,以及“视图构建”“动态 SQL 生成”“多阶特征计算”“滑动窗口”等关键能力。该框架支持全量历史重算与大规模 Shuffle 计算,通过高度配置化设计,将字段抽取、特征定义、策略判定彻底从代码中解耦,实现策略快速迭代与低成本上线。同时结合数据倾斜治理、列裁剪优化等工程实践,大幅提升稳定性与性能,成为风控体系的重要计算底座。

01 简介

在互联网业务高速发展的大背景下,作弊手段层出不穷,从恶意点击、流量造假,到批量刷单、黑产“薅羊毛”,手法不断翻新、隐蔽性持续增强。这些行为不仅侵蚀了平台的公平秩序,更直接带来显著的经济损失,并严重损害广告主利益和普通用户的体验与信任。因此,全方位、持续演进的反作弊能力已成为互联网产品生态稳定运行的关键基石。

百度基于以上问题构建了一套系统化的企业级反作弊系统,根据时效性和业务需求分为三类:在线反作弊、实时反作弊与离线反作弊。这三类反作弊能力相互补充,共同构建起完整的风控防线,但在防护策略、检测深度和业务价值上各有侧重。

在线反作弊主要负责毫秒级别的请求风险判定,适用于简单规则和轻量级指标,例如从请求头部字段、访问频率等维度快速判断风险,并结合 Redis 等缓存计算实现即时响应。这类机制非常适合于即时性要求极高的场景,例如登录请求拦截或简单阈值规则拦截,但受限于可实时访问的数据维度较少。

实时反作弊在此基础上,通过流式计算分析序列行为、业务上下文和多维特征,在秒级甚至分钟级实现更加精准的策略判定。实时系统能够响应更复杂的行为模式,例如账户连续异常操作、设备跨地域跳变等行为,兼具时效性与一定程度的特征深度,是在线与离线反作弊之间的关键桥梁。具体介绍见基于Flink的配置化实时反作弊系统

然而,在整个百度反作弊体系中,离线反作弊系统的战略价值与日俱上,是构建高精度模型、深度分析行为模式和提升整体风控能力的“底座“

与在线和实时系统相比,离线反作弊不受时效性的约束,可以充分利用完整历史数据进行大规模的批量分析与深度挖掘。其价值主要体现在以下几个方面:

  • 全面的数据视图:离线系统可以访问业务全量日志、用户历史轨迹、跨周期行为等丰富维度的数据,这些数据在在线场景中往往无法实时获取或难以完成整合。
  • 深度行为建模:通过对长期行为序列的分析,可以发现复杂的作弊模式,例如跨账号关联、长期周期异常趋势、人机行为判别等,这些模式在短周期内往往难以捕捉。
  • 特征工程与策略优化:离线挖掘计算出的高维特征是构建机器学习模型的基础,也是实时风控策略得以优化的重要来源。无论是统计类指标、聚合行为分布还是时序特征,这些信号都能够显著提升模型精度。
  • 黑产库与历史知识积累:离线分析能够构建不断增长的“黑产行为库”和风险特征库,支持跨业务线共享和复用。这种长期积累的“经验库”是在线/实时系统难以替代的。

正因如此,百度在反作弊领域投入多年经验,构建了高效的离线挖掘框架,用于批量处理用户行为日志、提取高维特征、训练模型并验证策略,为线上策略提供长期优化与精准判定的动力支持,使整套反作弊体系具备更强的防护能力和持续学习能力。本文介绍该离线挖掘框架的整体架构和设计亮点,并深度解读特征计算链路、性能优化实践以及配置化模块化能力,展示其在刷量识别、账号行为分析、广告作弊治理等场景中的工程价值。

02 离线挖掘框架解决的核心问题

2.1 成本和实现平衡

流式实现特征计算往往需要更高的计算成本,而对于大部分反作弊策略的实现并不需要极高的时效性要求,离线挖掘框架恰恰是解决流式运行高成本,高压力和运行时效进行平衡的媒介,小时级别的产出已可满足大部分业务需求。

2.2 全量历史重算能力和大规模Shuffle

离线的核心优势是:强全量能力 + 强历史回溯能力 + 强复杂聚合能力。

全量历史重算能力:

  • 可以直接扫描全量历史数据(天级、月级、年级)
  • 支持特征逻辑变更后的全量重算
  • 支持复杂回溯计算

大规模Shuffle:

  • 可以做大规模 Shuffle
  • 支持复杂 SQL(多层嵌套、窗口、分组)
  • 支持大表与大表 Join

2.3 多场景数据源和输出灵活对接

离线数据往往面临各种数据格式、表等复杂多样的数据源及灵活多变的输出格式。

  • 数据源类型:目前我们的框架现有数据源支持Turing表, UDW(hive)表, AFS(Parquet, CSV, Txt, PB)文件、用户自定义SQL等,并可以灵活增加wget接入数据源等功能。
  • 输出类型:对于输出也灵活实现了Turing表, UDW(hive)表, AFS(Parquet, CSV, Txt, PB)文件等格式功能,并可以增加输出至clickhouse、doris等存储媒介便于监控分析。
  • 多数据源输入:实现多种数据源同时输入解析,并支持对不同数据源分别清洗过滤,并支持对各数据源单独筛选 & 分区, 实现对不同数据的灵活操控。

03 反作弊离线挖掘框架介绍

3.1 离线挖掘整体框架

百度离线挖掘框架使用生效流程图如下:

图片

上图展示了离线挖掘框架在整个反作弊系统中的使用流程图,即框架在反作弊流程中的使用过程:

  • 用户在配置平台配置 数据源、特征、策略、输出维度等各项配置conf文件。
  • 用户通过配置平台打conf包到对应afs地址, 在TDS平台中筛选集群信息、资源配置等、读取conf配置文件, 并手动调起spark任务。
  • 离线挖掘框架会加载配置信息, 运行spark 任务, 任务结束后将结果输出到 AFS。
  • 用户使用一脉、Jupter等写ETL 任务评估策略是否符合预期, 若符合预期, 则将特征、策略配置上线, 否则修改特征、策略配置等重新运行。

具体离线挖掘框架流程图:

图片

上图展示了离线挖掘框架的整体流程图,分为 extractor 模块、accmulator 模块、joiner 模块、policy 模块等。

Extract (抽取)模块:

抽取(Extractor)模块是离线挖掘框架的数据入口与标准化核心,负责从原始日志或明细表中读取多源行为数据,按照既定 schema 进行字段筛选、类型转换、脏数据过滤和统一格式映射,将分散、异构的原始数据加工为结构清晰、字段规范、可计算的标准行为数据集;同时结合配置文件(如特征或字典配置)完成基础标签补充与维度对齐,为后续的视图构建与聚合计算提供稳定、统一的数据基础。

图片

这张图展示了抽取模块实现的功能:

  1. 输入数据:对原始输入数据源进行解析(包括Hive表,PB日志,parquet数据解析等)
  2. 解析特征配置文件:特征fea_001类型为segment(统计数据),维度为query,条件为:app_id=5&&city=‘北京’,即统计符合条件在app_id=5&&city=‘北京’的每个query的数量。同理特征fea_002为统计符合条件product_id不空的clkip的数量。
  3. 自定义字段:用户可以根据udf函数自定义所需要的字段。
  4. 结果数据:从日志中解析抽取出所有特征中所需要的字段,以图中示例结果为:fea_id,log_timestamp,query,app_id,agent_id,baiduid,product_id,…,其中log_timestamp为必输出数据。

除了 spark sql 支持的所有原生 functions 之外,结合业务实际使用场景,还支持了 多个自定义数据处理算子,并支持用户自定义udf扩展

图片

Accmulate (聚合)模块:

Accumulator(聚合)模块是整个系统的“计算引擎”,负责将海量的原始日志转化为具有统计意义的反作弊特征。基于指定维度和时间窗口对行为数据进行结构化聚合计算,将原始事件流转化为可用于策略判断和模型输入的指标特征。它支持多种聚合算子(如 count、sum、distinct 等)、条件过滤统计以及多维度分组能力,并通过状态管理机制维护窗口内历史数据,实现连续、可配置的特征生成。从工程视角来看,Accumulate 本质上是一个配置驱动的多维度窗口化统计计算模块,是连接原始行为数据与风险决策逻辑之间的关键桥梁。

以下是该模块的详细执行流程及功能解析:

图片

核心流程图解析
  • 数据准备:接收来自 Extractor 的标准化数据,并根据 feature.yml 加载特征定义。
  • 视图构建:这是 Themis 框架的特色,通过 View 和 DataView 概念,将数据按不同的维度(如 baiduid、IP、cookie)进行切分。
  • 动态 SQL 生成:框架不会硬编码聚合逻辑,而是根据配置动态拼接 Spark SQL 语句(如 SUM、COUNT、DISTINCT)。
  • 时间窗口:根据配置文件中的配置的时间窗口进行划分时间段
关键技术特性
  • 视图构建:视图构建,将同一批行为数据转换为带有“统计主体标识”的统一结构,从而支持多维度特征的动态聚合,是面向特征计算的维度抽象层。

在反作弊或行为分析场景中,同一条行为数据可以被多种“主体”统计,例如一条登录行为:

user_id,device_id,ip,cookie,ts

这条数据可以:统计到 user 维度、统计到 device 维度、统计到 ip 维度、统计到 cookie 维度,如果直接写 SQL 聚合,你需要:group by user_id,group by device_id,group by ip,… 。随着维度增加,代码会爆炸式增长。于是框架引入一个抽象,先构建一个逻辑视图,再根据视图去做聚合。

视图构建做了三件事:

  • 维度声明:将原始数据按指定字段组合成不同“统计视角”,这相当于提前确定这个特征是围绕谁统计的?
  • 维度映射:对应维度,记录对应的必要值,例如:(IP具体值,特征id)。
  • 维度参与聚合:不同统计维度通过 view_name / view_value 实现逻辑隔离。
  • 多阶特征计算:随着市场作弊手段的不断提高,普通的一阶策略已经无法识别潜藏的作弊数据,需要更高阶如三阶特征的策略来判定,并便于后期策略的多指标分析。

逻辑: 有些计费名(cntname)下不同的广告位区别很大,需要先算个tu维度的特征,然后tu维度又要先算下面的异常用户占比,就有了这个三阶特征。

例如:

  • 第一层为sn维度的普通比例特征,sn维度ip去重个数除以点击量的比例。
  • 第二层为tu维度,第一层的比例特征大于0.8的sn对应点击占tu全量点击的比例。
  • 第三层为计费名维度,第二层的比例特征大于0.4的tu对应点击占计费名全量点击的比例。

策略依赖的最终特征为计费名维度异常tu点击的比例,即第三层特征。

  • 数据倾斜治理:在聚合过程中,框架会根据配置文件设定开启/不开启识别热点 Key(如超大流量的 IP),广播热点数据,防止任务长尾,具体见4.2。

目前框架能够实现通用特征算子的新增和管理,目前已经支持的抽象化通用特征算子有以下 14 种:

图片

图中时间窗口windows逻辑解释:

在配置文件feature.yaml 中每个特征配置的字段

图片

支持大数据处理中经典的滚动窗口和滑动窗口模式

  • 滚动窗口定义:滚动窗口将每个元素指定给指定窗口大小的窗口。滚动窗口具有固定大小,且不重叠。例如,指定一个大小为 5 分钟的滚动窗口。在这种情况下,将每隔 5 分钟开启一个新的窗口,其中每一条数都会划分到唯一一个 5 分钟的窗口中,如下图所示。

图片

  • 滑动窗口定义:滑动窗口也是将元素指定给固定长度的窗口。与滚动窗口功能一样,也有窗口大小的概念。不一样的地方在于,滑动窗口有另一个参数控制窗口计算的频率(滑动窗口滑动的步长)。因此,如果滑动的步长小于窗口大小,则滑动窗口之间每个窗口是可以重叠。在这种情况下,一条数据就会分配到多个窗口当中。举例,有 10 分钟大小的窗口,滑动步长为 5 分钟。这样,每 5 分钟会划分一次窗口,这个窗口包含的数据是过去 10 分钟内的数据,如下图所示。

图片

Join (关联)模块:

Join(关联)模块是离线挖掘框架中的数据整合层,负责将来自不同视图或不同计算阶段产出的特征结果进行按键对齐与多维关联,通过统一主键(如 user_id、device_id、ip 等)将分散的聚合结果横向拼接成完整的特征宽表;同时处理字段冲突、空值补齐和粒度对齐等问题,确保不同维度、不同时间窗口的统计指标能够在同一维度下合并输出,为后续策略判定提供结构化综合特征数据集。具体是将抽取(Extract)模块与特征计算(Accmulate)模块数据关联, 并以logid进行Group By, 得到PV粒度全量数据, 将特征计算结果拼回各日志中,得到output2 结果 (产出为: log+ feature)。

图片

上图展示join模块的基本逻辑,即将特征聚合模块结果使用logid,拼接到原始日志中,使得抽取模块每条日志拼接到自己所命中的所有特征

  1. 对特征聚合模块(Accmulate)每条结果增加logid字段。
  2. 对特征聚合模块进行logid聚合,多个特征结果聚合到一条logid中。
  3. 抽取模块(Extract) 使用logid,Left join关联logid聚合后的特征聚合模块数据,得到joiner结果。
Policy (策略判定)模块:

Policy(策略判定)模块是离线挖掘框架中承接特征结果并输出最终风险结论的决策核心,负责将聚合产出的多维特征输入规则引擎或策略配置体系,根据预设阈值、组合条件与优先级逻辑进行匹配与计算,生成风险标签、命中规则、风险等级或处置建议;同时支持策略可配置化与版本管理,使风控逻辑能够在不改动底层计算代码的情况下灵活调整,实现特征到业务决策的闭环落地。该模块解析配置的策略文件policy.yaml, 根据policy_id 对 每条日志命中的features 进行策略判定, 输出最终结果,得到output3 结果 (产出为:  log + feature + policy)。

图片

这张图展示了反作弊规则的判定流程:

1.输入数据:每条日志包含多个字段,包括基础字段(如IP、手机号、UID等)、计算得到的特征(如统计特征fea1、fea2等)。

2.策略判定:系统基于预设的反作弊规则,对各字段、特征。例如,规则1要求【fea_001 > 100 && fea_002 < 10】,规则2要求 【IP like ‘192.%’ && fea_002 > 100】。多个规则都会执行判定逻辑,判断是否命中。

3.结果输出:最终的PV数据会带上反作弊命中结果。例如,在示例中,该PV数据命中了policy_002,表明该行为可能存在风险。

以上就是策略配置的所有介绍,通过配置化管理字段、特征、词表、模型和规则,反作弊系统能够快速响应业务需求,灵活调整检测逻辑。同时,配置化设计大幅降低了开发部署成本,提高了策略迭代效率。

3.2 流程汇总

以上3.1介绍了离线挖掘框架各个模块实现的功能,代码实现以scala的dataframe容器作为各个模块之间数据传输的媒介,此处以dataframe的计算步骤来汇总介绍框架是如何进行数据传输。

图片

04 离线挖掘框架设计亮点

4.1 模块化工程架构思想

框架整个代码实现力求模块化、轻量化;便于并行开发和测试,对后期维护升级铺平渠道。

图片

以上图为工程实现图,步骤解释:

  1. 通过TDS/spark-submit提交spark job

  2. runner调用context的init()方法,进行框架配置任务初始化

  3. init()过程中调用ConfLoader和DictLoader加载配置文件、词表,以及注册udf等等初始化操作

  4. init()返回封装好的context对象

5、6、7、8、执行各模块,将计算结果保存至context

9.根据配置的round轮数,输出对应结果的df

从运行图可以看到,这套离线反作弊挖掘框架并不是简单的“Spark 作业集合”,而是一个具备完整工程设计理念的 可编排计算引擎。其核心设计思想体现在四个方面:统一调度中枢、数据上下文抽象、算子标准化编排、配置驱动解耦

1. 统一调度中枢:构建“作业引擎”而非脚本集合

框架以 OfflineThemisRunner 作为唯一入口,负责生命周期管理、流程调度和执行编排。所有模块均由 Runner 驱动执行,而非模块间直接调用。体现“控制流集中管理,业务逻辑分散执行”。

工程优势:

  • 统一异常处理
  • 执行流程清晰、可追踪
  • 支持任务模板化和标准化运行

2. Context 抽象:解耦控制流与数据流

整个计算链路通过 Context 进行数据承载。各算子只与 Context 交互,而不直接依赖其他算子。

工程优势:

  • 消除模块间的强耦合
  • 实现数据语义统一管理
  • 支持中间结果复用与调试
  • 允许执行顺序灵活调整

从架构角度看,Context 是框架的“数据总线”,将数据流从算子依赖关系中剥离出来,使系统具备真正的模块化能力。

3. 算子标准化:构建可组合的计算流水线

框架将特征计算拆分为四类标准算子:Extractor(抽取)、Accumulator(聚合)、Joiner(拼接)、Policy(过滤)

所有算子遵循统一接口规范(run(context)),输入输出标准化,将复杂业务逻辑抽象为标准化计算单元

工程价值在于:

  • 新特征开发只需实现算子接口
  • 降低复杂链路的维护成本
  • 便于统一优化与性能调优
  1. 配置驱动:将策略从代码中剥离

通过配置来驱动计算流程和策略逻辑。代码负责能力,配置负责策略。

具体配置功能见4.3

4.2 运行优化

1、解决数据倾斜

在Accumulate特征聚合阶段,使用到groupby进行聚合操作,如果热key数据量大的情况下导致单个 Task 处理大量数据,即会出现严重的数据倾斜,甚至导致 OOM / 失败重算。

图片

以上图的优化思路:采样识别 + 拆分 Join (Skew Join)

  • 首先用 Spark API 的 sample() 统计左表 key 出现频次,先采样找出热点(大 Key)
  • 将左表按是否热点拆分
  • 将右表也对应拆分
  • 对热点 Key 用广播 Join ,避免 Shuffle
  • 非热点 Key 按常规 Join
  • 最后union all两份数据得到最终结果

对于采样解决数据倾斜已经配置化,用户可根据实际需求自定义配置是否启动优化和采样的比例,具体见4.3

2、列裁剪优化

Join拼接模块阶段,在优化前使用炸开后的Extract数据 Left Join Agg结果(view_name,view_value,window_start<=time_col<=window_end), 获取结果数据(Joiner), 结果数据包含(neededViews + agg聚合结果)。

我们假设:

1). 抽取出的Extract中含100个neededViews字段

2). feature.yml中feaList包含了80个featureId

那么就会出现以下情况:

1). 假设某条数据命中了50个feature条件,那么这条数据的聚合结果就有50条

2). 对Extract进行爆炸,也会爆成50条

注:

1). 以上方式使用Extract Left Join Agg结果时,每条数据会被扩充几十甚至上百倍,若每条Extract数据字段较多,则会造成很大的数据冗余,这些数据并不参与计算,浪费计算资源。

2). 因此再通过此方法进行group by聚合操作,浪费了很多不必要的内存,很容易发生数据倾斜,计算速度也会很慢。

图片

以上图为列裁剪后的优化,优化思路为:

其实优化前第一步的操作就是为了将logid赋值到每一条ACC特征计算结果上,那样接下来才能进行group by logid操作。

  1. 我们先对抽取模块结果列裁剪logid和关联键的hash()值,和特征计算模块同样的关联键的hash()进行join。

  2. 再对特征计算结果进行group by logid操作,就能减轻许多计算压力。

  3. 最后用Extract Left Join第2步的结果即可。

综上,经过列裁剪及聚合下沉操作后,实际工程速度在列数较多场景下均提升60%以上,并有效防止OOM,降低任务失败率。

4.3 配置化

为了满足反作弊策略快速上线、精细化模拟验证和灵活联调等高频迭代需求,我们的实时反作弊系统采用了高度配置化驱动架构,并将所有配置集中托管平台上进行统一管理。

在这一体系下,策略和计算逻辑不再硬编码到程序中,而是通过规范的配置文件描述出来,从字段抽取、特征定义、规则判定到结果产出,每一个步骤都可以通过配置完成。策略开发人员只需在平台上配置好各项参数,系统即可自动生成对应的作业,并支持一键打包和上线执行,大幅缩短了业务上线周期,降低了对底层框架开发的依赖。

图片

策略配置主要由以下几类配置模块组成:

主配置:全局环境配置,这是框架的主配置文件,定义了任务运行的基础环境和全局参数,控制任务的运行模式、资源分配和全局开关。

  • 输入输出:该配置决定了框架的输入地址、输入格式、输出地址、输出格式、控制框架需要的输出阶段等,例如round1,round2,round3。
  • 优化:还可在此配置中配置是否开启抽样优化及抽样的比例等。
  • udf自定义函数:用户可以自定义udf函数。

字段配置:负责将各种来源、各种格式的原始日志映射为框架可识别的标准字段。我们将字段抽取逻辑进行了配置化抽象,策略开发人员使用类似于写sql的方式即可完成简单字段的etl逻辑的开发,如常见的json字段抽取,字符串处理,反作弊内部的常用UDF等,配置能覆盖大部分字段抽取。根据抽取方式不同分为:

  • 基础字段:直接从原始数据流中提取的字段,例如设备 ID、用户 ID 等。
  • 二次计算字段:简单的字段转换逻辑(如 IP 转地域、UA 解析)。
  • 维表字段:通过查询词表映射关系获得的字段,例如黑名单匹配结果、分类标签等。

特征配置:特征是策略的重要判定依据,定义了如何从标准字段中计算出用于反作弊判定的统计特征。特征配置包括以下几个关键方面:

  • 特征类型:数据的聚合方式,如sum、count、distinct等。
  • 窗口信息:设置聚合特征的时间窗口范围和窗口形式,时间范围如:1 小时、1天等,窗口形式如:滑动窗口、滚动窗口等。
  • 特征维度:特征的聚合维度,如用户、设备、IP 地址等。

词表配置:词表通常是历史已知的黑名单、字段映射(如ip映射城市)等固定维表信息,在数据进入引擎之前,利用词表进行初步的“脏数据”清洗或黑名单过滤,提供外部参考数据,用于过滤或打标。配置内容需包括以下几个方面:

  • 词表路径:指定词表的存储位置,支持文件路径或分布式存储地址。
  • 词表类型:支持多种形式的词表,包括集合(set)、键值对映射(kv)、正则表达式(regex)等。

策略配置:规则配置决定了作弊行为的最终判定规则和处置方式,组合特征,输出最终的作弊名单或风险评分:

  • 策略判定阈值:定义触发策略的条件,例如基础字段匹配、词表匹配、风险评分的阈值、特征累积阈值、模型打分阈值等。
  • 策略判黑等级:设定风险等级,区分低、中、高风险及对应的处置措施。

以上总结配置文件的各个功能如下:

图片

05 总结

本文介绍了基于spark 的离线反作弊挖掘框架,围绕解决的基本问题、工程设计亮点等展开。通过特征计算和配置化管理,提升了反作弊系统的检测效率和稳定性。展望未来,离线反作弊挖掘框架将持续演进,与更多智能算法、大模型和业务系统深度融合,不断完善检测能力和可用性。借助持续优化的特征计算与策略模块,此框架将为百度生态提供更加坚实的反作弊保障。

GRAB:面向广告CTR预测的生成式排序框架,突破序列建模与泛化瓶颈

作者 百度Geek说
2026年2月5日 15:32

近日,百度商业技术团队释出生成式排序框架GRAB(Generative Ranking for Ads at Baidu)技术细节论文。传统深度学习推荐模型(DLRM)长期存在的泛化能力不足、行为序列建模瓶颈,百度商业技术团队以大语言模型(LLM)规模化经验为启发,推出生成式排序建模范式,将用户序列建模重塑为第一级结构。我们设计了因果动作感知多通道注意力(CamA)、先序列后表征训练(STS)等关键算法,实现了开箱式端到端序列化建模;线上结果显示,GRAB相较传统DLRM体系收入提升3.05%、CTR提升3.49%,并呈现出随交互序列、模型规模增长的稳定Scaling能力。

论文链接:[arxiv.org/abs/2602.01…]

中文解读:[微信公众号]

01 面向CTR预测的“生成式排序”新范式

长期以来,DLRM体系在广告推荐/排序场景中占据主流,但在复杂用户行为序列下,往往需要重度特征工程与稀疏/稠密特征协同,仍可能出现对长序列利用不足、跨场景泛化受限等问题。GRAB以端到端生成式框架重构CTR建模流程,通过统一建模与训练策略,增强对长历史交互信息的吸收能力,并将用户行为中的关键“动作信号”纳入因果视角下的注意力建模,以更稳定地刻画时序动态与意图演化。

http://oscimg.oschina.net/AiCreationDetail/up-7b20423443bfb5b463d4e3c254ff463d.png

△GRAB模型设计核心结构

02 三项关键创新:从结构到训练的系统性升级

1. 端到端生成式框架(End-to-End Generative Framework)将CTR预测问题重构生成式排序范式,降低对传统DLRM中显式特征工程与复杂组件堆叠的依赖,使整体建模路径更统一、更可扩展。

2. 因果动作感知多通道注意力(Causal Action-aware Multi-channel Attention, CamA)在多通道注意力结构中显式刻画用户行为序列中的动作信号及时空关系,更有效捕捉“时序动态 + 行为动作”的耦合信息,从而提升预测质量与稳定性。

3. 面向规模化的训练策略(Sequence-Then-Sparse, STS)提出“先序列、后稀疏(STS)”训练组织方式,在保证序列建模能力的同时兼顾稀疏特征与训练效率需求,为工业级大规模ID特征与自回归序列化训练与部署提供可落地的优化路径。

03 线上核心场景全量部署:收益与CTR实现稳定提升

在线上部署实验中,GRAB相较既有DLRM体系取得显著改进:收入提升3.05%CTR提升3.49%。同时,模型呈现出明确的Scaling-Law:随着纳入更长的用户交互序列,更大的模型尺寸,其表达能力提升表现为单调、近似线性增长,显示出对长序列信息的更强利用效率与更好的扩展潜力。

GRAB的价值不仅体现在指标提升,更在于其面向工业推荐系统的可扩展路径:通过生成式建模范式与推荐场景的结合,在“数据、计算、算法”的约束下,提供了可复用的算法框架与工程化实现方案,为后续更长上下文、更强泛化能力的广告排序模型演进奠定基础。

❌
❌