阅读视图

发现新文章,点击刷新页面。

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

导读 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

导读 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的配置化离线反作弊系统

导读 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预测的生成式排序框架,突破序列建模与泛化瓶颈

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

❌