阅读视图

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

基于Mach-O文件的动态库与静态库归属方案及API扫描实践

Mach-O简介:

Mach-O文件全称Mach Object,是在MacOS、iOS、iPadOS上的可执行文件,类似于Windows上PE文件。支持的CPU架构类型主要有x86_64、armv7、arm64。

Mach-O文件的生成过程 源代码-->预处理-->词法分析-->语法分析-->语义分析-->中间代码-->生成目标代码-->汇编-->机器码-->静态链接-->Mach-O文件

Mach-O能做什么

了解Mach-O格式的结构和加载过程,可以帮助我们更容易的理解APP的启动过程、C函数的hook、动态库的懒加载等原理。常见的应用场景有:

① Crash的符号化

② Bitcode分析

③ APP启动速度的优化

④ 优化APP包体积

⑤ 方法调用链分析

接下来,本文针对第5种应用场景,介绍下工作中用到的两个实践项目。

1.基于Mach-O文件的动态库与静态库的归属方案。

2.基于Mach-O的API扫描方案。

由于目前APP Store上基本已废弃对armv7的支持,所以接下来介绍的方案都是基于arm64架构的Mach-O分析。

实践项目一:基于Mach-O文件的动态库与静态库归属方案

背景:大部分的APP都会包含多个动态库与静态库,之家APP也一样,并且随着业务的增长,APP内集成的功能越来越多,静态库和动态库的数量也在不断增加。为了提升用户体验,之家APP进行了多个维度的数据采集,如:网络、崩溃、卡顿、秒开、图片性能等,而采集后的数据如何精准、快速的分发到研发人员进行解决,一直是我们面对的难题。

基于Mach-O结构与Runtime的原理,我们通过不断实践,实现了一套自定义归属方案,此方案可以对性能数据进行库的归属划分,再通过库归属找到开发人员,从而解决分发难题。

下面对此方案做一个详细说明。

首先,归属划分主要涉及动态库与静态库两种场景:

(1)运行时创建的某个对象归属于哪个库(通过类查找库)。

(2)公共库的某个方法被哪个库所调用(通过堆栈查找库)。

因为动态库其本身就是代码隔离的,查找非常方便,而静态库最终会被编译到主程序的二进制文件中,或者多个静态库被包到一个动态库中,所以无法直接进行类和堆栈的归属划分。

接下来本段落会先介绍下动态库的归属方法,然后重点介绍静态库的归属方法。

动态库:

类定位:先用对象查找isa指针获取类,然后使用类查找所在的可执行文件即可;

NSBundle *bundle = [NSBundle bundleForClass:objClass];

堆栈定位:获取的堆栈可以直接区分出所归属的动态库,如图:

NSArray<NSNumber *> *callAddresses = [NSThread callStackReturnAddresses];
long long callStackAddress = [callAddresses[i] longLongValue];
Dl_info info = {0};
dladdr((void *)callStackAddress, &info); //获取堆栈地址对应的可执行文件信息
NSString *dliFname = [NSString stringWithFormat:@"%s",info.dli_fname]; //取出库名

静态库:

名词解释:

Mac服务器:用于编译APP和dSYM的符号解析,以下简称Mac服务器。

日志服务器:记录线上APP上报的性能数据,以下简称日志服务器。

ASLR:全称Address spce layout randomization,地址空间布局随机化,通过对堆、栈、共享库等关键数据区域的地址空间随机化,防止攻击者直接定位代码位置来篡改程序。这种技术会使得APP或者动态库每次运行加载到内存中时的基地址都是随机的。

静态库归属常见的方案是基于dSYM的符号解析,主要流程:

1、打包时在Mac服务器上存储所有库的dSYM文件。

2、线上APP上报执行文件名称、发布版本、偏移量等信息。

3、日志服务端收到APP的上报信息,通过版本号、执行文件名称查找缓存的dSYM,最后在Mac服务器上使用偏移量进行dSYM符号解析,返回静态库名。

这种方案需要在Mac服务器上对所有动态库的所有版本dSYM进行缓存,增大了服务器的存储成本,并且由于需要三端的交互才能完成静态库的归属,稳定性较差,同时在APP运行时无法做到直接定位,可读性也不高。

针对dSYM符号解析的问题,我们通过分析Mach-O的结构与原理,探索出了基于Mach-O的静态库归属方案,具体如下:

在编译期通过解析Mach-O和Link Map文件,生成静态库的类地址区间和汇编代码段的地址区间,在运行时根据isa指针(指向了Mach-O中的类声明地址)和代码偏移地址解析出静态库名。

先解析Mach-O文件的结构,从Mach-O的头部开始,Header中包含了二进制文件的大小、支持的CPU类型、Load Commands的数量和大小,Segment中包括了各Section和符号表等的偏移位置和大小。

Mach-O的结构比较复杂,Segment现在已知的类型有50多种,不同类型职责不同。

该方案只用到了代码段和类列表,所以只对__text和__objc_classlist进行解析。

解析Mach-O头部,关键代码如下:

mach_header_64 mhHeader;
//读取头部信息
[fileData getBytes:&mhHeader range:NSMakeRange(0, sizeof(mach_header_64))];
for (int i = 0; i < mhHeader.ncmds; i++) {
    load_command* cmd = (load_command *)malloc(sizeof(load_command));
    //读取Load_command
    [fileData getBytes:cmd range:NSMakeRange(currentLcLocation, sizeof(load_command))];
    if (cmd->cmd == LC_SEGMENT_64) {
      segment_command_64 segmentCommand;
      [fileData getBytes:&segmentCommand range:NSMakeRange(currentLcLocation, sizeof(segment_command_64))];
      NSString *segName = [NSString stringWithFormat:@"%s",segmentCommand.segname];
      //提取汇编代码 __TEXT
      if ([segName isEqualToString:SEGMENT_TEXT] || [segName isEqualToString:SEGMENT_BD_TEXT]) {
         section_64 sectionHeader;
         [fileData getBytes:§ionHeader range:NSMakeRange(currentSecLocation, sizeof(section_64))];
         NSString *secName = [[NSString alloc] initWithUTF8String:sectionHeader.sectname];
      }else if ([segName isEqualToString:SEGMENT_DATA]) {
         //提取指定DATA sectionHeader信息
         unsigned long long currentSecLocation = currentLcLocation + sizeof(segment_command_64);
      }
    //符号表
    }else if (cmd->cmd ==LC_SYMTAB){
       symtab_command  tsymtabcommand;
       [fileData getBytes:&tsymtabcommand range:NSMakeRange(currentLcLocation, sizeof(segment_command_64))];
       symtabcommand = tsymtabcommand;
    }else if(cmd->cmd == LC_FUNCTION_STARTS){
       [fileData getBytes:&funcStartHeader range:NSMakeRange(currentLcLocation, sizeof(linkedit_data_command))];
    }
}

如果对每个类都标记库名会使生成的ClassMap文件会过大,对包体积影响较大。通过分析APP的编译过程,发现静态库是顺序编译,在每个Section下静态库也都是分段的,所以最终通过计算每个静态库的偏移地址和大小来生成静态库的位置标记。

  • 静态库类的标记: 解析Mach-O的ClassList段,借助LinkMap文件,反向解析出每个静态库的类声明位置,找到第一个类的声明地址为起始地址,最后一个类的声明地址+类声明的字节大小为类声明的结束地址。
  • 静态库代码段标记:原理与查找类声明类似,结合__TEXT、Symbol Table和Function Starts, 找到静态库第一个类的第一个方法的起始地址作为库的代码段起始地址,找到静态库的最后一个类的最后一个方法的结束地址,作为静态库代码段的结束地址。

通过上面两步生成ClassMap和TextMap导入到ipa中,文件小于1kb,对APP大小几乎无影响。

生成脚本的位置要在Link Binary With Libraries之后,Copy bundle Resources之前。

前面介绍了编译期的工作,下面介绍下运行时的定位原理。

1.运行时获取对象对应的isa指针,找到class,再通过class指针地址减去动态库加载到内存的起始地址,算出对应的偏移量(上面提到的ASLR,会使每次运行APP的内存基地址发生改变,所以需要计算偏移地址),然后使用偏移量去ClassMap中找到对应的静态库名。

NSString *imageName = [[NSString alloc] initWithUTF8String:_dyld_get_image_name(i)];
//找到APP主二进制
if (_dyld_get_image_header(i)->filetype == MH_EXECUTE &&
[imageName containsString:mainBundle.executablePath]) {
  mainExecuteAddress = _dyld_get_image_vmaddr_slide(i);
    break;
}
uintptr_t os = (uintptr_t)objClass;
//计算出类的偏移地址
uintptr_t classInBundleAddress = os-mainExcuteAddress;
NSDictionary *classMap = [self pluginAllAddressWithClass];
for (NSString * key in classMap.allKeys) {
    NSDictionary *addressDic =  classMap[key];
    if (addressDic && [addressDic[@"end"] longLongValue]>classInBundleAddress && classInBundleAddress>=[addressDic[@"start"] longLongValue]) {
        return key;
    }
}

2.基于堆栈定位静态库。首先获取到无符号的堆栈数组,然后找到上一个调用的callback地址,检索到堆栈地址所在动态库起始地址,使用栈地址-动态库起始地址得到偏移量,再用偏移量去TextMap中找到对应的静态库名。

NSBundle *mainBundle = [NSBundle mainBundle];
Dl_info info = {0};
//获取堆栈地址对应的插件信息
dladdr((void *)callStackAddress, &info);
//获取二进制名
NSString *dliFname = [NSString stringWithFormat:@"%s",info.dli_fname];
//获取偏移量
uintptr_t callStackBundleAddress = callStackAddress-[self mainStartAddress];
NSDictionary *textMap = [self pluginAllAddressWithText];
for (NSString * key in textMap.allKeys) {
NSDictionary *addressDic = textMap[key];
//查找所属静态库
   if ([addressDic[@"end"] longLongValue]>callStackBundleAddress &&callStackBundleAddress>=[addressDic[@"start"] longLongValue])
    {
         return key;
    }
}

这样静态库的归属便可以在运行时完成,耗时小于1ms,可以大范围应用于APP各种场景中。

小结:基于Mach-O文件的动态库与静态库归属方案介绍完了,在静态库归属上,相比之前dSYM的方案,它的复杂度更低、易用性更高,可以在运行时实时解析,而且更容易迁移到 其它 APP上使用。

实践项目二:基于Mach-O的API的扫描

背景:在实际工作中,由于经常需要定位APP中调用过的API,为了减少重复的工作,我们实现了一套自动扫描的工具。

API扫描常见的方案是基于语法树的扫描,代码在编译时会生成语法树,通过遍历语法树可以实现API的扫描。

由于语法树扫描存在以下缺点,无法满足使用需求。

1、不支持黑盒扫描,语法树是在编译时才能生成,所以无法扫描三方SDK。

2、扫描速度太慢,语法树扫描的功能强大但是在扫描性能上较低,对2万行代码树的扫描在优化的情况下也需要1分钟左右的时间。

为了解决上面的两个问题,我们实现了基于Mach-O的API扫描方案。

实践项目一中介绍的Mach-O结构,本段落还会继续用到

__objc_classrefs:类引用列表。

__objc_selrefs:方法引用列表。

Mach-O解析的主要步骤:

1、首先解析__objc_classrefs,读取所有调用的外部API,检索被扫描API的类地址记录为class_addr。

2、解析__objc_selrefs,检索被扫描API的方法地址记录为method_addr。

3、把二进制机器码解析出汇编代码,找到所有的bl指令,计算bl指令最近的x1寄存器的值,对比x1寄存器与method_addr是否相等,相等则记录bl指令的位置。

4、在bl指令的位置向上检索是否有要找的类地址class_addr,一直检索到最近的函数入口,找到则输出结果,未找到进入第5步。

5、找到调用者类的起始和结束地址,检索起始与结束地址的所有汇编指令是否存在被扫描API的类,出现则输出结果。

API扫描流程图

反汇编代码:

vm:(unsigned long long)vm {
   mach_header_64 mhHeader;
   //解析头部
   [fileData getBytes:&mhHeader range:NSMakeRange(0, sizeof(mach_header_64))];
   // 获取汇编代码的偏移地址和大小
   char *ot_sect = (char *)[fileData bytes] + begin;
   uint64_t ot_addr = vm + begin;
   csh cs_handle = 0;
   cs_insn *cs_insn = NULL;
   cs_err cserr;
   if ((cserr = cs_open(CS_ARCH_ARM64, CS_MODE_ARM, &cs_handle)) != CS_ERR_OK ) {
       NSLog(@"未能初始化: %d, %s.", cserr, cs_strerror(cs_errno(cs_handle)));
       return NULL;
   }
   // 设置解析模式
   cs_option(cs_handle, CS_OPT_DETAIL, CS_OPT_ON);
   cs_option(cs_handle, CS_OPT_SKIPDATA, CS_OPT_ON);
   // 反汇编
   size_t disasm_count = cs_disasm(cs_handle, (const uint8_t *)ot_sect, size, ot_addr, 0, &cs_insn);
   if (disasm_count < 1 ) {
       NSLog(@"汇编指令解析不符合预期!");
       return NULL;
   }
   return cs_insn;
}

检索方法的范围:

//检索方法偏移范围
do {
       @autoreleasepool {
           unsigned long long index = (end - textList.addr) / 4;
           char *dataStr = s_cs_insn[index].mnemonic;
           //查找是否是函数跳转指令,记录方法的开始和结束地址
           if (strcmp(dataStr, "b")== 0|| strcmp(dataStr, "ret")==0) {
               unsigned long long nextSymoble = end + 4;
               MethodHelper *nextMethodHelper = [objectSymbolMap objectForKey:[NSNumber numberWithUnsignedLong:nextSymoble]];
               if (nextMethodHelper && ![nextMethodHelper.className isEqualToString:className]){
                   //找到类的最后一个函数地址作为类的结束地址
                   callClassHelper.end = end;
                   return callClassHelper;
               }
           }
           end += 4;
       }
   } while (end <= textList.addr + textList.size);

查找objc_msgSend调用位置,检索调用的外部方法名。

依据runtime的原理,objc_msgSend调用时x1寄存器是存放的方法地址,所以主要查找bl指令前的x1寄存器信息。

在扫描过程中发现,在调用objc_msgSend时的寄存器有时是需要ldr指令计算得出,这里涉及到汇编中高低位地址的查找。

汇编指令中会把地址拆分为高低位,所以检索时要注意x1寄存器值的计算过程。如上图所示,需要计算x8寄存器+低位地址0x908,然后对比被检测API的地址与x1寄存器是否相等,最后再向上查找被检测API的类,如果匹配则输出结果。

小结: 至此基于Mach-O的API扫描介绍完了,Mach-O的扫描方式相比传统的语法树扫描,在本质上脱离了代码,可以对任意的动态库和静态库进行扫描,而且扫描过程可以采用多线程分段扫描进行提速,能够在2秒内完成对3万行代码编译产物的扫描,相比语法树扫描在速度上有20倍以上的提升。

在语言上除了对OC方法的扫描,还支持Swift、C方法的扫描,原理与OC扫描类似,这里就不做详细介绍。

总结与展望:

以上是基于Mach-O结构的动态库与静态库的归属和API的扫描方案,已应用于生产环境,助力团队降本提效。

未来我们还会持续在以下方面进行探索与实践:

1、扩展归属检索的范围,如类别、block、常量、C方法等。

2、API使用规范的扫描,基于Mach-O生成简易的调用关系图来查看API的规范情况,可扫描冲突的分类方法,提前发现隐藏的bug。

3、安全方面:防止反编译、动态注入。在iOS应用安全方面,除了通过Mach-O分析来防止反编译和动态注入,开发者还可以使用专业的混淆工具如IpaGuard来加强保护。IpaGuard是一款强大的iOS IPA文件混淆工具,无需源码即可对代码和资源进行混淆加密,支持多种开发平台,有效增加反编译难度。

参考文档:

Overview of the Mach-O Executable Format (apple.com)

Introduction (apple.com)

开源一年,我的 AI 全栈项目 AI 协同编辑器终于有 1.1 k star了 😍😍😍

大家好 👋,我是 Moment,目前正在使用 Next.js、NestJS、LangChain 开发 DocFlow。这是一个面向 AI 场景的协同文档平台,集成了基于 Tiptap 的富文本编辑、NestJS 后端服务、实时协作与智能化工作流等核心模块。

在这个项目的持续打磨过程中,我积累了不少实战经验,不只是 Tiptap 的深度定制、编辑器性能优化和协同方案设计,也包括前端工程化建设、React 源码理解以及复杂项目架构实践。

如果你对 AI 全栈开发、文档编辑器、前端工程化或者 React 源码相关内容感兴趣,欢迎添加我的微信 yunmz777 一起交流。觉得项目还不错的话,也欢迎给 DocFlow 点个 star ⭐

image.png

📖 简介

DocFlow 是一款面向团队协作的块级文档编辑器。它融合了 Notion 的灵活性与飞书的协作能力,通过块级内容架构、实时协同编辑和 AI 辅助功能,帮助团队高效完成文档创作与知识管理。

我们希望通过技术手段减少协作摩擦,让文档编辑更接近团队的真实工作流。无论是产品规划文档、技术方案设计,还是会议记录整理,DocFlow 都能提供流畅的创作体验。

✨ 核心特性

DocFlow 参考了 Notion 与飞书的设计理念,将内容以块为单位进行组织。每个块都是独立的编辑单元,可以灵活组合与调整,同时支持实时协作与 AI 辅助。

  • 🧱 块级编辑器:支持文本、标题、列表、代码块、表格、图片、视频等 20+ 种内容类型,通过拖拽即可调整块级元素的顺序与层级关系。

  • ⚡ 实时协作:基于 Yjs CRDT 算法实现多人同步编辑,自动处理编辑冲突。支持实时光标跟踪、成员在线状态与历史版本回溯。

  • 🤖 AI 功能:内置 AI 助手,支持头脑风暴、内容润色、文档续写与智能问答。可根据上下文生成结构化内容建议。

技术选型

DocFlow 采用全栈 TypeScript 架构,前端基于 Next.js 构建,后端使用 NestJS 框架。通过统一的类型系统和现代化的工程实践,保证了代码质量与开发效率。

🎨 前端架构 (Client-side)

Next.js

项目基于 Next.js App Router 架构,利用 React Server Components 优化首屏渲染性能。通过 Server Actions 实现前后端通信,确保类型安全的同时简化了数据流转。

Tiptap

编辑器核心采用 Tiptap 框架,基于 ProseMirror 构建。通过扩展机制实现了丰富的块级编辑能力,支持自定义节点与快捷命令,为用户提供接近 Notion 的编辑体验。

Yjs

协作功能基于 Yjs CRDT 算法实现,能够自动处理多人编辑时的冲突,保证数据最终一致性。配合 Awareness 模块,实现了实时光标追踪与在线状态同步。

⚙️ 后端架构 (Server-side)

NestJS & Prisma

后端使用 NestJS 模块化框架,通过依赖注入实现业务逻辑解耦。Prisma ORM 提供类型安全的数据访问层,支持高效的数据库查询与迁移管理。

Hocuspocus

Hocuspocus 作为 Yjs 的 WebSocket 服务端,负责协调文档协作会话,处理客户端连接与数据同步。通过拦截器机制实现权限控制与数据持久化。

Prometheus & Grafana

集成 Prometheus 进行指标采集,通过 Grafana 可视化展示系统运行状态。监控包括 API 响应时间、数据库查询性能、WebSocket 连接数等核心指标。

20260203091658

Grafana 监控面板实时展示系统各项性能指标,包括请求量、响应时间、错误率等关键数据,帮助快速定位性能瓶颈。

ELK Stack (Elasticsearch & Kibana)

使用 Elasticsearch 存储和检索日志数据,Kibana 提供日志分析与可视化能力。支持全文搜索、日志聚合与异常检测,便于问题排查与系统审计。

日志分析系统

Kibana 日志分析界面,支持按时间、日志级别、服务模块等维度查询和过滤日志,提供结构化的问题排查路径。

MinIO & RabbitMQ

MinIO 提供对象存储服务,用于存储用户上传的图片、视频等文件。RabbitMQ 作为消息队列,处理异步任务如图片压缩、邮件发送等,避免阻塞主业务流程。

功能介绍

DocFlow 将 AI 能力集成到编辑器中,通过理解文档上下文来辅助内容创作。AI 不是简单的文本生成工具,而是能够理解语义、提供决策建议的智能助手。

AI 头脑风暴

当你有一个初步想法但不知如何展开时,AI 头脑风暴可以帮助拓展思路。输入核心概念后,AI 会从不同角度生成 3-6 个结构化方案,每个方案都包含具体的实施思路。

AI 头脑风暴输入界面

在编辑器中输入头脑风暴主题,AI 会基于输入内容理解你的需求场景。

AI 头脑风暴结果展示

AI 生成的多个方案以卡片形式展示,每个方案都有清晰的标题和详细说明。你可以选择任意方案插入到文档中,或者继续优化调整。

这不只是简单的内容生成,AI 会根据上下文理解你的意图。无论是产品功能设计、内容分类规划,还是业务流程优化,AI 都能提供可行的思路参考,帮助快速决策。

AI 文本润色

AI 文本润色功能

选中需要优化的文本段落,AI 会分析文本结构与表达方式,提供更清晰、更专业的改写建议。支持调整语气风格,如正式、简洁、友好等。

AI 续写

AI 续写功能会根据前文内容自然延续写作。当前文内容较长时,系统通过 RAG (检索增强生成) 技术,从文档中检索相关段落,确保续写内容与上下文保持逻辑一致,避免偏离主题。

AI 续写功能演示

AI 续写时会参考前文的写作风格、用词习惯和逻辑结构,生成连贯自然的后续内容。你可以继续编辑生成的文本,或者重新生成。

AI 聊天

目前 AI 聊天功能作为独立页面存在,后续会集成到编辑器侧边栏,与文档内容深度关联。未来计划实现 Agent 模式,类似 Cursor 那样能够自动编辑文档内容。

7a8ba58a4ab3b592bb7fae1b45634648

协同编辑

多人协同编辑

多人同时编辑时,每个用户都有独立的光标颜色标识。文档修改实时同步,冲突自动合并。右侧显示当前在线成员列表与他们的编辑位置。

未来计划

DocFlow 将持续优化协作体验与 AI 能力,同时加强工程化建设,提升系统可扩展性。

🏗️ 工程化体系深度重构

  • 迈向 Monorepo 架构:计划基于 pnpm workspaces 和 Turborepo 将项目重构为 Monorepo。前后端代码分离,共享类型定义与工具函数,提升代码复用率与构建效率。

  • 组件库与插件生态开放:将 Tiptap 自定义扩展(如代码沙箱、交互式图表等)提取为独立 npm 包,开放给社区使用。同时建立插件开发规范,支持第三方开发者扩展编辑器能力。

🎙️ 多维协同体验升级

  • 集成 LiveKit 实时音视频:在文档协作场景中引入实时音视频通话。团队成员可以边看文档边讨论,提升复杂决策场景下的沟通效率。

LiveKit 集成方案

  • 实时群聊系统:在文档侧边栏集成实时聊天功能,支持针对文档内容发起讨论。消息可以关联到具体的文档块,形成完整的协作反馈闭环。

🤖 智能内核的跨越式进化

  • 基于 RAG 的私有知识库:引入 RAG (Retrieval-Augmented Generation) 技术,让 AI 能够检索用户的历史文档。AI 回答问题时会参考团队沉淀的知识资产,提供更精准的决策支持。

  • 从 Copilot 迈向 Agent:探索 AI Agent 在文档场景的应用。未来 AI 将能够自主执行任务,例如从会议纪要中提取待办事项,自动同步到第三方工具,实现从辅助创作到自动化办公的升级。

🚀 快速开始

环境要求

  • Node.js >= 24
  • pnpm >= 10.28.2

本地开发

  1. 克隆仓库
git clone https://github.com/xun082/DocFlow.git
cd DocFlow
  1. 安装依赖
pnpm install
  1. 启动开发服务器
pnpm dev
  1. 打开浏览器访问
http://localhost:3000

🐳 Docker 部署

方式一:使用 Docker Compose(推荐)

# 使用预构建镜像
docker-compose up -d

# 访问应用
http://localhost:3000

方式二:手动构建

  1. 构建镜像
docker build -t docflow:latest .
  1. 运行容器
docker run -d \
  --name docflow \
  -p 3000:3000 \
  -e NODE_ENV=production \
  docflow:latest
  1. 访问应用
http://localhost:3000

健康检查

容器内置健康检查端点:

curl http://localhost:3000/api/health

🤝 贡献指南

欢迎提交 Issue 和 Pull Request!

在提交代码前,请确保:

  • 运行 pnpm type-check 通过类型检查

  • 运行 pnpm lint 通过代码检查

  • 运行 pnpm format 格式化代码

  • 遵循项目的代码规范和提交规范

详见 CONTRIBUTING.md

📬 联系方式

搞懂 Cursor 后,我一行代码都不敲了《实战篇》

Hi~大家好呀,我是清汤饺子。

前两篇我们分别讲了 Cursor 的基础用法和进阶功能——怎么安装、怎么用 Agent、怎么让 AI 更懂你的项目。

好!那这篇文章是第三篇,也是这个系列的最后一篇。

我们来聊聊怎么让整个团队高效地用好 Cursor。

如果你是团队的负责人,或者你想推动团队一起用 Cursor——这篇文章就是给你看的~

这个系列一共三篇:

第一篇从零上手 Cursor

讲讲怎么安装、Agent 怎么用、怎么写功能、怎么修 Bug

第二篇:让 Cursor 更懂你

上下文引用、Rules、Skills、MCP 这些

第三篇(就是这篇):团队协作与场景实战

怎么在团队里用好 Cursor


我踩过的坑

说出来你们可能不信,我们团队刚开始用 Cursor 的时候,那叫一个乱。

每个人都自己用自己的,也没有什么规范。

有人说用 Composer 1.5,有人说用 Opus;有人让 AI 随便改代码,有人全程盯着。

结果呢?代码风格不一致,AI 生成的代码质量参差不齐,有时候还闹出安全问题。

累不累啊。

后来我们痛定思痛,开始系统性地在团队里推广 Cursor。

这篇文章,就是把我们团队踩过的坑、总结的经验都分享出来。希望能帮你们少走弯路。


一、GitHub / GitLab 集成:让 AI 进入代码审查流程

1.1 我们遇到了什么问题

以前每次 PR 提交后,都要排队等同事 Code Review。有时候同事忙,等个大半天甚至一天都正常。

而且 Review 质量也看心情——有时候认真看,有时候赶时间就随便扫一眼。低级错误反复出现。

1.2 BugBot 是什么

Cursor 的 GitHub 集成可以让你每次 PR 都有 AI 先审一遍。

我叫它BugBot——它会在每次 PR 更新时自动分析 diff,找出 Bug,安全问题和代码质量问题,直接标注在有问题的代码行上,并给出修复建议。

相当于有个人帮你先过一遍,把明显的问题都标出来。

1.3 怎么开启

  1. 打开 cursor.com/dashboard?tab=integrations
  2. 点击 GitHub 旁边的 Connect
  3. 选择授权范围
  4. 进入 Bugbot 标签页,选择要开启的仓库

1.4 触发方式

  • 自动触发:每次 PR 更新时自动运行
  • 手动触发:在 PR 评论中写 cursor review

💡 心得:BugBot 帮我挡掉了至少一半的低级错误。之前每次 Code Review 都要说"这里变量名不一致"、"这里没判空",现在 AI 替我说了。

1.5 @cursor 触发 Agent

接入 GitHub 之后,任何人都可以在 PR 或 Issue 的评论中输入 @cursor + 描述,直接触发一个 Cloud Agent:

# 在 Issue 评论中
@cursor 实现用户头像上传功能

# 在 PR 评论中
@cursor 修复这个 Bug

二、Cloud Agent:让 AI 在云端跑任务

2.1 本地运行的痛

以前我让 AI 干活,有个最大的限制——必须开着电脑。

有时候下班了或者出差了,想让 AI 跑个大任务,根本不可能。

而且有些任务耗时特别长,比如跑完整的测试套件、构建生产环境——电脑一直开着不现实,电费也肉疼。

2.2 Cloud Agent 是什么

Cloud Agent 把这些工作搬到云端的虚拟机上,让 AI 在你睡觉的时候继续干活。

说人话:就是找了个远程电脑帮你跑 AI 任务,关机也不耽误。

每个 Cloud Agent 实例:

  • 可以启动开发服务器、打开浏览器
  • 从 GitHub 克隆仓库,在独立分支上工作
  • 可以运行构建、测试,自动修复 CI 失败
  • 支持 MCP 工具,可连接数据库

2.3 怎么启动

入口 方式
Cursor 编辑器 Agent 输入框下拉菜单选择 Cloud
Cursor Web cursor.com/agents
GitHub PR 或 Issue 评论中 @cursor
CLI agent -c "任务描述"

2.4 Automations:定时和事件驱动

Automations 让 Cloud Agent 在没有人工触发的情况下自动运行。

说人话:就像设了个定时闹钟,到点了 AI 自己就跑去干活。

实用场景示例:

# 场景一:每晚自动清理 Feature Flags
触发器:每天凌晨 2 点
Prompt:扫描代码库,找出已全量放量超过 30 天的 Feature Flag

# 场景二:PR 合并后自动更新文档
触发器:PR merged
Prompt:分析本次合并的改动,更新 docs/ 目录

💡 心得:我现在每天下班前设一个 Automation,第二天来公司 AI 已经把测试跑完了、报告打好了——的感觉不要太爽。


三、团队管理:成员、权限与用量

3.1 创建团队

访问 cursor.com/team/new-team,填写团队名称,邀请成员。

按量计费,不按席位。 这点很划算——人多人少都是一个价。

3.2 角色体系

角色 使用 Cursor 管理成员 是否计费
Member 可邀请
Admin 完整权限
Unpaid Admin 完整权限

Unpaid Admin 是专为 IT、财务等管理人员设计的角色——不用付费,但是可以管理成员。

3.3 用量分析(Analytics)

Analytics 仪表盘能看到:

  • AI Share of Committed Code:AI 生成代码占提交代码的比例
  • Agent Edits:Agent 建议的代码行数及用户接受率
  • Usage Leaderboard:团队内各成员的使用量排行
  • Repository Insights:按仓库维度统计的 AI 代码贡献

💡 心得:我们团队每个月会看一次 Analytics。之前大家都不好意思说用多了,现在数据透明,反而更愿意分享技巧了。

3.4 SSO 配置

Cursor 支持 SAML 2.0 SSO,Team 计划免费包含。

配置步骤:

  1. Dashboard → Settings → Single Sign-On (SSO)
  2. 在身份提供商中创建 SAML 应用
  3. 完成域名验证

四、前端工作流实战

4.1 我们遇到的前端痛点

前端开发有几个典型的高频痛点:

  • 设计稿还原效率低
  • 组件复用不规范
  • 调试 UI 要反复截图对比

4.2 从设计到代码的完整闭环

配合 Figma MCP,可以让 Cursor 直接读取设计文件:

@figma 读取设计文件中的 UserCard 组件规范,
用 Tailwind CSS 实现这个组件
用浏览器工具验证还原效果

实现后,让 Agent 用浏览器工具直接对比:

@browser 打开 localhost:3000/components/user-card,
截图后与设计稿对比

💡 心得:之前调样式要在 Figma 和代码之间来回切换几十次——现在 AI 帮我对比,效率直接翻倍。

4.3 用 Cursor 做原型设计

快速原型是前端开发中 Cursor 价值最高的场景之一:

我需要一个任务管理应用原型:
- 任务列表支持拖拽排序
- 支持按优先级筛选
- 样式参考 Linear 简洁风格

小提示

  • 指定具体的 UI 库(shadcn/ui、Radix)
  • 完成后立即用浏览器工具验证响应式表现

五、Python / 数据分析实战

5.1 Python 项目的挑战

Python 项目的挑战在于:

  • 环境依赖复杂
  • 数据分析代码散乱难维护
  • 调试 pandas 问题要反复 print

5.2 配置 Python 专属 Rules

.cursor/rules/ 下创建规则文件。

---
description: "Python 项目规范"
globs: "**/*.py"
---

## 代码规范
- 所有函数必须有类型注解
- 使用 pydantic BaseModel 定义数据结构

5.3 数据分析场景技巧

让 AI 理解数据结构:

这是销售数据,帮我:
1. 检查数据质量
2. 按地区做销售额汇总
3. 画出趋势图

💡 心得:之前数据分析都要先写一堆 print 看数据——现在直接让 AI 帮我分析,它还能给出优化建议。


六、用 AI 写文档和测试

6.1 文档和测试是最大的拖延源

你们有没有这种感觉——每次写文档,加测试,都是能拖就拖?

我懂。写代码已经够累了,还要写文档,加测试,太烦了。

但文档和测试又特别重要,怎么办?

让 AI 帮你写啊!

6.2 自动生成代码注释

注释生成策略:

不要让 AI 给每一行加注释——那样只会产生噪音。更好的方式是聚焦在真正需要解释的地方:

为这个文件中的所有公共函数生成 JSDoc 注释

小提示

  • 明确指定注释风格(JSDoc / Google style)
  • 在 Rules 中固定注释规范

6.3 单元测试生成

从 Bug 反向生成测试:

这个 PR 修复了优惠券叠加计算错误的 Bug,
根据修复的逻辑,生成回归测试

💡 心得:修完 Bug 立即让 AI 生成测试,已经成了我的习惯。现在代码质量放心多了。


小结

这篇文章覆盖了团队级 AI 协作的完整体系:

模块 解决的团队问题 核心工具
GitHub/GitLab 集成 代码审查效率低 BugBot、@cursor
Cloud Agent 任务依赖本地机器 Cloud Agent、Automations
团队管理 成员权限混乱 角色体系、Analytics、SSO
前端实战 设计还原慢 Figma MCP、Browser 工具
Python 实战 数据分析代码质量低 数据库 MCP
文档与测试 文档滞后 CLI 批处理、Automations

团队落地的推进路径

第一步(第 1-2 周):建立基础

  • 创建团队,邀请成员,配置 SSO
  • 接入 GitHub,开启 BugBot
  • 在一个活跃仓库配置 Project Rules

第二步(第 3-4 周):引入自动化

  • 配置 Cloud Agent 环境
  • 创建第一个 Automation
  • 用 Analytics 建立基准数据

第三步(第 2-3 个月):深化场景

  • 添加对应的 MCP(Figma、数据库)
  • 为高频重复任务创建 Automations
  • 封装团队专属 Skills

第四步(持续优化):数据驱动改进

  • 每月查看 Analytics
  • 收集大家踩过的坑,及时更新规则

三篇系列回顾

至此,整个「Cursor 从零到精通」系列完整收官。

篇目 核心主题
第一篇 从零上手
第二篇 让 Cursor 更懂你
第三篇 团队协作与场景实战

三篇叠加在一起,描述的是同一件事的三个层次:会用 → 用好 → 团队用好。

从只会问 AI "帮我写代码",到真正把 AI 融入团队的开发基础设施——不只是提升个人效率,而是让整个团队的协作方式升级。

这才是 Cursor 作为 AI 编辑器真正的价值所在。


好了,这个系列到这里就完结啦!

觉得有帮助的话,点个赞收藏一下,后续更新也能第一时间看到~

感谢你陪我一起走完这三篇文章,有问题欢迎在评论区问我~

我们下个系列见!👋

也欢迎关注我的公众号「清汤饺子」,获取更多技术干货!

吃龙虾🦞咯!万字拆解OpenClaw的架构与设计 | 掘金一周 3.19

本文字数1400+ ,阅读时间大约需要 5分钟。

【掘金一周】本期亮点:

上榜规则:文章发布时间在本期「掘金一周」发布时间的前一周内;且符合各个栏目的内容定位和要求。 如发现文章有抄袭、洗稿等违反社区规则的行为,将取消当期及后续上榜资格。

一周“金”选

image.png

内容评审们会在过去的一周内对社区深度技术好文进行挖掘和筛选,优质的技术文章有机会出现在下方榜单中,排名不分先后。

前端

用 Three.js 写了一个《我的世界》,结果老外差点给我众筹做游戏? @何贤

SimplexNoise + fbm 构建的地形则显得更加真实,这里可以简单的理解为 SimplexNoise 负责的是粗略的山峰和山谷的构建,而 fbm 则负责在这个大体基础上构建更加真实的地理细节

嗯…微信小程序主包又双叒叕不够用了!!! @古茗前端团队

随着移动设备硬件性能的提升以及微信版本的不断升级,用户设备对ES6及以上语法的支持度已显著提高。在这一背景下,大量为兼容ES5而引入的降级与垫片代码逐渐失去必要性,反而成为包体体积的负担,具备明确的优化空间。

断网也能装包? 我在物理隔离内网搭了一套完整的私有npm仓库 @LiuMingXin

下一步需要做的就是怎么把verdaccio整体部署到内网机器了,只要把verdaccio移入到内网机器,启动起来,后续只需要 更新storage和.verdaccio-db.json就可以实现依赖的更新了。

后端

用这个框架彻底摆脱Controller,从此专注业务——ArcRoute @一只叫煤球的猫

ArcRoute 内置了一条统一调用链:参数解析 → 校验 → 前置处理 → 业务调用 → 后置处理 → 响应包装。这意味着,很多原本散落在各个 Controller / Advice / Interceptor 里的重复逻辑,可以被整合成一条清晰、可插拔的管道。

Android

你还用 IDE 吗? AI 狂欢时代下 Cursor 慌了, JetBrains 等 IDE 的未来是什么? @恋猫de小郭

IDE 不再是开发中的关键环境,它的作用越来越弱,而强大的 Agent 重要性也越来越明显,你的产品除了要有优秀的模式,还需要有更前沿的 Agent 才能留得住用户。

谷歌 Genkit Dart 正式发布:现在可以使用 Dart 和 Flutter 构建全栈 AI 应用 @恋猫de小郭

Genkit 内置支持 LLM 工具调用,自带了 Agent 能力的适配场景,也是用一个 Agent 开发框架 ,通过 Action 和 Tool 的抽象,你可以定义一系列函数(比如查询数据库、发邮件、搜索网页),模型可以根据用户意图自主决定调用哪些工具

从零构建用于 Android 开发的 MCP 服务:原理、实践与工程思考 @fundroid

我们将在 main.py 中继续添加代码,实现所有工具。所有工具都将定义在 start_server 函数内部,以便访问 mcp 实例和共享的 temp_dir

详解 Compose background 的重组陷阱 @RockByte

在 Kotlin 中,给函数传递一个普通参数(如 Modifier.background(color)),参数的值必须在函数调用时(即 Composition 阶段)就被计算出来。这就迫使你在重组的时候读取了状态。这也会导致状态的变更会发生重组。

人工智能

吃龙虾🦞咯!万字拆解OpenClaw的架构与设计 @摸鱼的春哥

Gateway(网关层):作为整个系统的控制大脑。它负责维护 WebSocket 控制平面,进行全局的会话管理(Session),并决定消息如何被路由(Routing)到正确的目的地。

OpenClaw 完全指南:这可能是全网最新最全的系统化教程了! @ConardLi

OpenClaw 内置了持久化记忆系统,通过 Markdown 文件和向量数据库存储长期记忆。它采用 “向量 + 关键词” 的混合检索策略,既能通过语义匹配召回久远对话,也能精确提取实体信息,并支持跨会话、跨项目的记忆延续。

OpenClaw macOS 完整安装与本地模型配置教程(实战版) @吴佳浩

Skills 安装失败不影响主程序正常聊天。如果不需要这些特定功能,直接跳过即可。需要的话去 App Store 把 Xcode 更新到 16.4+ 再重新运行 openclaw skills install <n>

📖 投稿专区

大家可以在评论区推荐认为不错的文章,并附上链接和推荐理由,有机会呈现在下一期。文章创建日期必须在下期掘金一周发布前一周以内;可以推荐自己的文章、也可以推荐他人的文章。

大禹平台:流批一体离线Dump平台的设计与应用|得物技术

一、前言

大禹平台是一个离线 Dump 平台。在不同的场景都有自己的 Dump 流程,我们这里的 Dump 特指在搜索、推荐、广告(后续简称 “搜推广”)的场景中,将异构数据源加工处理后给到索引平台做索引的流程。

Dump 流程有如下一些特点:

  • 多源异构的数据:包括 MySQL、ODPS、HBase 和 Kafka 等各种数据源。
  • 多样化的输出:输出支持搜推广引擎构建倒排索引、Summary 服务构建 kv/kkv 索引等。
  • 流批数据结合:一般会有全量和增量,需要保证处理逻辑一致,增量能达到秒级更新。
  • 数据处理能力:例如多表 Join、UDF、Filter 等,以方便业务的开发和接入。

离线 Dump 流程

二、项目背景

现状

当前 dump 开发模式

如上图是当前常见的 Dump 开发模式,采用了流批分离架构:流处理通过 DTS 订阅 binlog,由 Flink 消费主表变更事件并反查关联表构建宽表,实现增量更新;批处理则将 MySQL 数据抽取至 ODPS,通过 Spark 处理多源数据并按业务逻辑拼接,最终输出 ODPS 表。这种架构存在以下问题:

当前 dump 开发的问题

目标

依托社区搜索核心场景,构建流批一体化的新质 Dump 架构,实现以下三大核心能力突破:

  • 工程效率: 基于可视化 DAG 编排工具,提供低代码开发能力,通过拖拽式界面实现复杂任务流程的快速搭建与迭代,显著降低开发门槛。
  • 数据质量: 基于流批一体架构,通过统一逻辑开发范式实现流批数据同源同构,从根本上提升数据准确性与可靠性。
  • 稳定性保障: 通过引入镜像表和状态大宽表,提高了数据的查询效率,系统性降低对源库的反查压力,确保系统长期稳定运行。

二、大禹平台介绍

平台设计

系统架构

平台架构

如上图是大禹平台技术架构,底层依赖公司的 DJob Cron 定时任务、Flink/Spark 流批计算能力以及多种存储系统;上层为平台支持的搜推广多种场景业务。

大禹平台分为管理平台与后台系统两部分。管理平台完成处理逻辑的 DAG 开发和相关 Debug、回归验证、监控大盘等能力;后台系统将管理平台的配置转为执行任务,然后依托流批框架生成 Flink/Spark 执行实例,通过调度引擎完成全流程任务执行。

如下图是新版 Dump 流程,将 Dump 拆分为三个阶段:镜像阶段、宽表阶段、导出阶段,以及流、批两种处理模式。新版流程处理过程有如下优化:

  • MySQL 镜像至 HBase: 平台将任务依赖的 MySQL 数据统一同步至 HBase 构建镜像层,实现与上游 RDS 解耦。有效规避多任务并发反查导致的数据库压力,支持跨任务共享复用 HBase 镜像表,显著提升数据源稳定性与资源利用率。
  • Binlog 订阅平台化: 将 RDS Binlog 订阅流程深度内嵌,自动完成 DTS 订阅创建与 Kafka 资源申请,封装为标准化服务。开发者无需关注底层链路,一键配置即可获取实时变更流,降低接入复杂度,保障流式数据可靠性。
  • 状态大宽表消除反查: 基于 HBase 构建持久化状态大宽表,完整记录字段中间状态。任务处理时直接读取状态数据,彻底规避冗余反查逻辑,简化开发流程。

新版 Dump 流程

调度引擎

大禹平台利用得物 DJob Cron 自建调度系统,通过搭建多个 Cron Job 轮训的方式,完成对任务分阶段的处理。

Cron Job 构建调度系统

一个执行实例的全流程

执行框架

在镜像、宽表、导出三个阶段,分别都有对应 Spark 和 Flink 处理框架。其中,镜像阶段完成 MySQL 数据同步,导出阶段完成状态宽表到引擎数据源的导出流程,宽表阶段是具体的业务逻辑实现。

宽表 Spark 框架逻辑: 任务严格遵循 DAG 拓扑顺序,依次执行各算子节点(数据源→业务逻辑→导出)的数据处理流水线,最终通过 BulkLoad 方式将结果高效写入 HBase。

宽表阶段 Spark 框架逻辑

宽表 Flink 框架逻辑: 消费非维表节点的增量,依据节点依赖关系进行拓扑排序后依次执行各节点计算逻辑,将产出字段更新至状态宽表,并实时同步至下游导出链路。

宽表阶段 Flink 框架逻辑

流批一体保障数据质量

平台采用统一的 DAG 编排引擎,将流处理与批处理任务抽象为相同的计算拓扑,从架构层面保障数据源头的天然一致性,彻底规避因不同环境下开发导致的数据偏差风险。同时,平台内置标准化的 UDF(用户自定义函数)开发模板与运行时框架:开发者只需专注业务逻辑实现,编写的 UDF 代码经一次注册,即可无缝嵌入流式与批量处理流程,真正实现 “一次开发、流批复用”,显著提升开发效率,降低维护成本,保障 Dump 开发从数据源头到处理逻辑各环节的流批一致性。

平台通过定义 AlgoDumpUDF 方法类,完成消息类型封装,用户可以利用 UDF 实现数据过滤和驱动删除等逻辑。

public abstract class AlgoDumpUDF implements UDFFunction, Serializable {
    //消息类型 add/delete/drop 三种
    public AlgoDumpMessageType algoDumpMessageType = 
    AlgoDumpMessageType.MESSAGE_TYPE_ADD;
    @Override
    public AlgoDumpMessageType getStatus() {
        return algoDumpMessageType;
    }


    //调用该方法实现增量驱动删除
    @Override
    public void delete(Object key, String reason) {
        this.algoDumpMessageType = AlgoDumpMessageType.MESSAGE_TYPE_DELETE;
    }
    //调用该方法实现增量过滤
    @Override
    public void drop(Object key, String reason) {
        this.algoDumpMessageType = AlgoDumpMessageType.MESSAGE_TYPE_DROP;
    }
    /**
     * 用户重写该方法完成业务逻辑开发
     */
    public void process() throws Exception {
    }
}

CASE示例:用户通过重写process()方法, 实现自己的业务逻辑,实现时可以利用drop方法把无效数据过滤,利用delete方法实现对下游索引发送删除消息。

public class MyUdf extends AlgoDumpUDF implements Serializable {
    public  Tuple2<String, String> process(String id, String taskname) 
    throws Exception {
        //过滤消息
        if(StringUtils.isBlank(id)) {
            this.drop(id, "drop by id null");
        }


        //驱动增量删除消息
        if(id.equals(0)) {
            this.delete(id, "delete by id = 0");
        }


        //用户写具体业务逻辑
        String a1 = "";
        if (taskname.equals("dddddd")) {
            a1 = "ddd";
        }
        String b1 = "test";
        return new Tuple2<>(a1, b1);
    }
}

小全量模式加速数据Dump

大禹支持任务实例按照大全量和小全量两种模式运行,针对部分频繁更新部分字段需求的任务可实现快速加载。

  • 大全量: 对数据源执行全量同步重建,生成全新的状态大宽表,并同步刷新流批处理链路,实现数据基准的彻底更新与端到端一致性保障。
  • 小全量: 基于现有状态大宽表,仅针对批处理来源字段加载最新数据源快照,经处理后通过 BulkLoad 高效写入 HBase;依托 HBase 多版本特性实现新旧数据平滑切换,确保批处理数据增量更新过程中查询服务零中断、数据时效性与业务连续性兼得。

小全量模式

任务复用支持数据分层管理

大禹平台支持任务产出的双重应用:既可对接计算引擎(如 CEngine),亦可作为公共数据被下游任务高效复用。平台通过标准化的 MirrorOut(导出)与 MirrorIn(接入)算子构建清晰的数据复用链路 —— 上游任务将公共数据配置为 MirrorOut 导出,下游任务通过 MirrorIn 算子一键引用,无需重复开发与数据搬运,实现数据资产的即产即用、任务依赖的显式管理,显著提升开发效率与数据复用性。

任务复用

管理平台

任务开发与运维

管理平台提供一站式任务开发生命周期管理,涵盖任务创建、可视化流程编排、实例调度与资源管控等核心环节;其中,Dump 任务通过可视化编排实现业务配置——用户仅需拖拽算子节点、配置参数,即可直观构建数据处理逻辑,显著提升开发效率与配置准确性。

如下图,通过拖拽算子的方式,可以直观地构建 dump 任务的流程图,实现便捷高效的开发体验。

图画编排式开发任务

执行实例以可视化流程图形式完整呈现任务执行全流程,每个节点清晰展示输入参数与输出结果,并支持对指定节点进行手动重试或终止操作,便于问题定位与流程干预。

执行实例状态

辅助工具

数据回归验证

平台提供流批数据回归验证能力,支持模板化配置与一键复用,高效保障数据质量与业务稳定性。

  • 批量回归: 多版本批数据快速比对,一键校验全量一致性,适用于版本迭代验证;
  • 流式回归: 基于索引表增量变更抽样,对指定时间窗口内实时数据进行跨索引一致性校验,精准定位流式链路异常。

创建批数据回归任务

创建流数据回归任务

数据Debug

大禹平台构建了覆盖全链路的数据运维干预能力,确保数据处理的可靠性与灵活性。

  • 组图配置: 支持对源端组图配置进行主动干预与调整,实现配置策略的快速生效。
  • Dump流程: 支持Dump构建流程的调控,实现对全链路流程的问题快速定位,保障数据产出的稳定性与高效性。
  • 在线索引: 提供线上索引数据的实时干预能力,支持对增量数据进行修正,确保索引内容的及时性与准确性。

四、业务场景实践

社区搜索倒排表链路

如下图所示,社区搜索倒排表 Dump 任务以动态内容为核心实体,融合动态实时内容流、天级统计特征及商品多维特征,通过流批一体处理生成高时效的倒排索引宽表。

社区搜索倒排宽表链路

穿搭精选推荐链路

如下图所示,穿搭精选推荐 Dump 任务以动态-商品关系为核心主表,融合动态维度的多源流批特征数据(如内容特征基础表、内容审核表、天级离线统计特征表等),利用DAG 编排构建动态-商品的大宽表。

穿搭精选推荐链路

五、未来规划

平台能力持续增强

  • 算子体系完善: 基于业务场景持续增强关键算子(如维表动态更新、Service 服务化算子、UDTF 部署优化等)和优化调度流程,强化数据处理灵活性;
  • 性能深度优化: 引入任务剪枝、智能倾斜治理等策略,提升资源利用率与执行效率;
  • 可观测性升级: 构建覆盖全局大盘与任务粒度的监控体系,完善资源消耗追踪、Debug 与全链路 Trace 能力,夯实平台稳定性与运维支撑基础。

深化协同共建,释放平台价值

  • 纵向提效: 聚焦索引构建效率攻坚,与索引平台深度协同重构数据同步链路。以社区搜索大宽表为例,当前同步耗时近3小时,通过消除冗余中间状态、精简处理流程,可以实现索引构建端到端提速,显著压缩数据准备周期。
  • 横向赋能: 平台能力已在社区域多业务场景完成验证,后续可以联动其他业务场景共建;同时平台的子功能也具有通用能力,可将数据回归验证、索引监控大盘等高复用能力模块化开放,赋能各业务线“即插即用”,加速技术资产沉淀与跨域协同创新。

大禹未来规划

往期回顾

1.基于 Cursor Agent 的流水线 AI CR 实践|得物技术

2.从IDE到Terminal:适合后端宝宝体质的Claude Code工作流|得物技术

3.AI编程能力边界探索:基于 Claude Code 的 Spec Coding 项目实战|得物技术

4.搜索 C++ 引擎回归能力建设:从自测到工程化准出|得物技术

5.得物社区搜推公式融合调参框架-加乘树3.0实战

文 /野雨

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

我拿到了腾讯QClaw的内测码,然后沉默了。

你好呀,我是歪歪。

在前几天 OpenClaw 火的一塌糊涂的时候,各大厂商都疯狂蹭热度,纷纷推出了自己一键安装版本的“龙虾”。

歪师傅知道的就有腾讯的 QClaw,智谱的 AutoClaw、字节的 ArkClaw、百度的 DuClaw、月之暗面的 Kimi Claw、MiniMax 的 MaxClaw 等等。

其中最特殊的我觉得是腾讯的 QClaw。

就是这玩意:

抛开具体的模型不说,其他的各类产品至少都是支持直接下载,给点免费额度,然后开箱即用。

但是就腾讯的 QClaw 它要内测,必须要邀请码,这就激起了我的好奇心了。

歪师傅就是容易“犯贱”。

可以直接下载的不用,反而非得想体验一下这个必须要排队等邀请码的。

其实在这之前我已经在一台闲置的 Windows 电脑上安装好了原生的 Openclaw。

那台电脑确实是上了年纪了,我安装的时候也是遇到了各种各样的麻烦,我就这么给你说吧:我第一天没装上,第二天还是没装上,最后想着反正是闲置的电脑,里面也没啥数据,索性直接重新装了一遍系统,之后才丝滑装好。

当然了,我这个属于个例。

我相信如果在我的主力机上装的话,肯定也会非常丝滑。

当我一切就绪,坐在电脑面前,通过飞书发送一条消息,让它操作我的电脑,在桌面写一个文档,最后顺利完成的那一瞬间的感觉,怎么说呢?

说实在的,那一瞬间,我的内心毫无波动,甚至还有点想笑。

我折腾了一番之后,确实有了一个“龙虾”,但是我确实是没有找到合适的使用场景。

有折腾的这个时间,我觉得我甚至不如去学学如何更好的使用 Claude Code。

我也是被巨大的流量蒙住了双眼。

看着大家都在讨论这个东西,也想着去看看。

但实际上,就像我前面说的,在折腾它之前,我完全没有想过我的使用场景是什么。

总之,我目前觉得对我个人而言,之前花在折腾 OpenClaw 的时间不太值得。

在一切顺利的情况下,不管你是在 Windows 还是 Mac 上装 OpenClaw,都不麻烦。

但是我作为一个相关从业者,在一台闲置的 Windows 电脑上装的时候都费了不小的劲儿。

更别说一些纯小白用户了。

所以,前面提到的各大厂商就是看到了这个痛点。

纷纷推出了自己的一键安装版本的“OpenClaw”。

只不过这个“OpenClaw”换上了各大厂的“专属皮肤”和“专用通道”。

对于想要体验一下的小白用户来说,我觉得够用了。

而这里面,我觉得对于小白用户来说,其中最值得期待的就是腾讯的 QClaw。

因为它的优势太明显了:微信对话框就是它的入口。

微信,国民软件了。

上到九十九,下到刚会走,这个范围内的人,可能没有飞书、没有钉钉、没有 QQ,但是大概率会有一个微信。

可以说是用户体验成本极低了。

但是这玩意它现在还处于内测阶段。

诶,巧了。

昨天,歪师傅申请到了一个内测码:

在 QClaw 的安装过程的页面,我看到了这样一句话:

默认大模型下限时免费提供国产优质稳定模型

其实我是有点纳闷的。

腾讯自研的大模型不就是混元大模型吗。

QClaw 这玩意肯定是使用自家的混元大模型呀,难道还会去用一下其他友商的免费额度?

不知道为什么不直接提到混元大模型,给自家的产品露露脸。

安装完成,输入内测码之后,你就可以通过扫码关联微信,然后就能通过手机操控电脑了:

但是在这个页面,也有一句话值得注意:

扫码后可通过微信与客服号对话发送指令操控电脑。

客服号,在微信中也就是以这样的形态存在的:

它并不是以一个微信好友的形式存在。

你不能把它拉进某个群里,也不能让它帮你回复一些微信消息。

那能不能在我登录了电脑微信的情况下,直接把电脑上的文件通过文件传输助手直接发给我呢?

这其实是我很关心的一个功能。

然而,很遗憾,并不能:

在官方的案例中,传文件是需要通过邮箱传递的,我觉得这个方案绕了一大圈,就没去尝试:

接着,我给了它一个简单的任务,测试一下它在电脑本地创建文件的能力:

这是一个基础能力,它并没有翻车。

这是它生成的文档:

但是它并没有把文档发给我:

前面说了,它目前没有这个能力。

随后,我又硬想出了一个这样的应用场景:

因为微信的提醒是强提醒,所以我可以让它提醒我按时喝水和不要久坐。

看它的项目路径下确实多了一个 jobs.json 的配置文件。

里面的内容点开看也是没毛病的:

就看明天会不会正常触发了。

按理来说这个不会有什么大问题,定时触发也算是“龙虾”的一个基础功能了。

我还测试了一下,让它打开微信公众号页面,把登录二维码截图发给我。

它是这样回复我的:

而我把类似的任务扔给我安装的原生 OpenClaw,大模型使用的是 MiniMax M2.5。

也是在没有安装任何 Skills 的情况下,它能截图并尝试通过飞书发给我。

我只是引导它,让它把图片变成互联网可访问的链接:

它就能直接去想办法,最后找到了一个我都不知道的免费的临时文件托管服务,搞定了我看不到图片的问题:

所以,“原生虾”和“封装虾”之间还是有些差异。当然了,最大的差异点还是在使用不同的大模型上。

QClaw 从功能上看,也是支持自定义大模型的:

但是,可能是我的打开方式不对。

我买的 MiniMax 的 Coding Plan 月度套餐,对应的 API Key 配上去之后,它识别不出来具体是什么模型。

我输入“MiniMax M2.5”呢,它又说不知道这个模型:

导致我用不了自定义大模型。

总之,我觉得目前的 QClaw 能力还很弱,和我心目中“QClaw” 还有一定的距离,还需要继续努力,拿出一些“诚意”出来。

巧了,刚写完这句话没几分钟,我就收到了 QClaw 的更新提示:

更新完成之后,我发现多了这几个东西:

首先是在右上角多了一个“龙虾管家安全沙箱”,点过去之后是“腾讯电脑管家”的页面:

呃,怎么说呢?

引流嘛,不寒碜。

左下角多了一个“灵感广场”:

这里面就是各种各样的官方 Skills。

而输入框里面的“虾灵感”,里面默认了几个选项,其实就是教你如何使用“灵感广场”里面的技能:

比如,选“学习打卡督促”:

最后,还多了一个“设置”页面:

是的,在刚开始的版本里面,连个设置入口都没有。

在设置里面,可以看到 Token 的消耗:

还可以进行一些技能管理:

在记忆模块里面,可以写一些全局的 prompt 提示词:

通过这次更新,可以看到腾讯电脑管家在 QClaw 这个产品上在持续发力。

只要在内部赛马比赛中能突出重围,在后续一些大的迭代中推出一些解决用户实际痛点的功能,会进一步巩固微信生态。

但是有一说一,难,很难,我感觉 OpenClaw 这波热度很快就要过去了。

当热度过去之后 QClaw 不知道将会何去何从。

现在的 QClaw 确实还很稚嫩,它像是一个刚学会走路的孩子,有着无限可能,但还需要时间成长。

不能传文件、不能进群聊、不能帮我回微信,但是这些“不能”的背后,其实还有一个更深层的问题:

当 AI 能力被植入到一个已经高度成熟的产品生态中,它的定位是什么?

是做一个安静的助手,还是成为一个 AI 好友,或者是其他什么形态?

最后,我还是推荐你去下载一个 QClaw 体验一下,毕竟在你下载完成扫码授权之后,它就活在你的微信里面,你不需要下载其他任何的 APP 就能进行体验。

方便程度还是很高的。

写在最后

其实在 OpenClaw 的这波浪潮之中,我感觉整个网络环境对于 OpenClaw 是很亢奋的,它确实完全破圈了,有好几个不做技术的朋友都来问我 OpenClaw 相关的事情。

但是整个过程我又很平静,远远没有我第一次使用 Claude Code,它给我解决实际问题后的兴奋。以及我周末的早上都不睡懒觉,就想去电脑前把玩它的新鲜感。

而 OpenClaw 呢?

我觉得回看 OpenClaw 这波浪潮,最值得玩味的不是技术本身,而是我们面对新技术时的集体心理,特别是这个新技术还达到了全民讨论的高度。

从无数人熬夜折腾安装配置,到上门收费安装“龙虾”,再到大厂以地推的方式免费安装,最后各大厂连夜推出的一键安装“龙虾”,这里面藏着一个很有趣的现象:

我们追逐的往往不是工具本身,而是一种“不能掉队”的安全感。

这种安全感焦虑,在上面一波又一波的媒体造势中,被放大了无数倍。

我也不想掉队,所以我在还没想清楚应用场景的情况下就去养了一只“龙虾”,配置完成的那一刻,我会有一瞬间的感觉到:真好,这波我又没有掉队。

这是一种自欺欺人的感觉。

但是这也没什么不好。

人嘛,总要靠点错觉活着。

只要别一直活在错觉里就行。

MiniMax 发布 M2.7,Agent 开始走向自我进化

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优化和协作功能的实现等核心难点。

如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。

如果你对 AI全栈 感兴趣,也欢迎添加我微信,我拉你进交流群

3月18日,MiniMax 发布新一代 Agent 旗舰大模型 M2.7。如果只看表面,这像是一次常规的模型升级。但从公开信息来看,M2.7 真正值得关注的,不只是分数更高、能力更强,而是它首次对外展示了一条更具代表性的技术路线,也就是"模型自我进化"。

根据 3 月 18 日公开报道,M2.7 通过构建 Agent Harness 体系,让模型深度参与自身训练与优化流程。在部分研发场景中,这套机制已经可以承担 30% 到 50% 的工作量,并在内部评测集上带来约 30% 的效果提升。这个表述背后释放出的信号很明确,AI 正在从"回答问题的模型"迈向"能够参与迭代自身能力的系统"。

这次发布最重要的,不只是性能提升

过去很长一段时间,行业讨论大模型,重点往往集中在参数规模、训练成本、推理速度以及 benchmark 排名上。但 Agent 时代的竞争逻辑已经开始变化。真正决定模型价值的,越来越不是单点能力,而是它能不能进入真实工作流,承担连续任务,并在执行过程中形成可积累、可复用、可优化的闭环。

M2.7 这次最大的不同,就在于它不再只是研发流程中的被优化对象,而开始成为研发流程中的参与者。

所谓 Agent Harness,可以把它理解为围绕 Agent 构建的一整套执行、反馈、优化机制。模型不只是完成任务,还会进入任务分解、流程回放、错误暴露、策略修正和样本反馈等环节。这样做的意义不在于概念新,而在于它把模型能力提升从"一次性训练结果"推进到了"持续演化过程"。

换句话说,过去我们更熟悉的是"训练一个更强的模型",而 M2.7 想展示的是"让模型参与把自己变得更强"。

工程能力,已经开始逼近一线水位

从公开成绩来看,M2.7 在工程场景上的表现是这次发布的另一大看点。

根据当日披露数据,M2.7SWE-bench Pro 上取得了 56.22% 的成绩。这个指标之所以重要,是因为它衡量的不是简单补全代码,而是真实软件工程环境中的问题理解、代码修改、上下文追踪和任务闭环能力。能在这个测试里打出有竞争力的成绩,说明模型已经不只是"会写代码",而是更接近"能参与工程"。

与此同时,M2.7VIBE-ProTerminal Bench 2 等更接近真实研发流程的测试中也有突出表现。公开说法中提到,它已经能够支持端到端项目交付与复杂系统理解。这一点比单个 benchmark 分数更值得重视,因为真实企业环境看重的从来不是一道题做对,而是模型能否在复杂上下文里持续完成任务。

从研发团队视角看,这意味着 Agent 的角色正在发生变化。它不再只是辅助写一段函数、解释一条报错,而是开始承担更完整的工作单元,比如理解项目结构、分析系统依赖、处理跨文件修改,甚至在终端和工程环境中完成连续操作。

如果这个趋势持续下去,开发团队对 AI 的期待也会随之改变。未来最有价值的模型,不一定是最会答题的那个,而是最能稳定交付结果的那个。

办公场景,开始成为另一条主战线

除了工程能力,M2.7 在办公场景上的提升也非常值得注意。

公开信息显示,它在 GDPval-AA 上取得了 1495 的 ELO 得分,并被描述为开源最高。同时,模型在 Office 文档处理、多轮编辑、复杂内容整理等任务上的表现也有明显增强。

这背后其实说明了一件事,MiniMax 对 M2.7 的定位,并不是单纯的代码模型,而是更偏向通用生产力 Agent。它既要能进入开发流程,也要能进入知识工作和协作流程。因为在真实企业场景里,研发、产品、运营、文档、汇报、分析并不是割裂存在的,大家需要的是一个能够跨场景接手任务的系统,而不是一个只能在单点场景里亮眼的模型。

从这个角度看,办公能力的提升并不是"附加项",而是 Agent 真正走向大规模落地的必要条件。

为什么“自我进化”这四个字值得单独拎出来看

这次发布里,最值得继续观察的,仍然是"模型自我进化"这条路线。

过去行业谈 Agent,经常会关注几个关键词,比如工具调用、长任务拆解、环境感知、记忆能力、多智能体协作。这些能力当然都很重要,但如果只停留在"会不会调用工具"这一层,Agent 的上限其实并不高。

更深的问题在于,当模型已经能完成任务之后,它能不能利用任务执行过程反过来优化自己。

如果答案是可以,那么大模型的发展路径就会发生结构性变化。未来领先的,不只是训练出一个更强基础模型的公司,而是能建立一套完整演化系统的公司。模型做任务,任务产反馈,反馈进入优化,优化再反哺下一轮任务执行。这样的闭环一旦跑顺,AI 的进步速度就不再完全依赖人工标注和传统训练流程,而会更多来自系统自身在真实世界里的持续学习能力。

这也是 M2.7 这次发布最有想象空间的地方。它传递的已经不是简单的"又一个更强模型来了",而是 Agent 正在从工具形态向系统形态迁移。

这次发布意味着什么

M2.7 目前已经在 MiniMax Agent 与开放平台上线。对开发者来说,这意味着相关能力不再只是实验室概念,而是已经开始进入可调用、可接入、可验证的产品阶段。对行业来说,这次发布的意义可能也不止于一次模型升级。

它更像一个明确信号,AI 竞争正在从"谁的模型更会说"进入"谁的系统更会做"。而在"会做"之后,下一个更关键的问题就是,谁能最先构建出真正有效的自我演化闭环。

如果说过去的大模型更像工具,那么 M2.7 想证明的是,Agent 正在变成系统。再往前一步,它甚至可能变成一种具备持续自我改进能力的数字生产力基础设施。

这或许才是 3 月 18 日这场发布最值得被记住的地方。

全面解析WhatsApp Web抓包:原理、工具与安全

“全面解析WhatsApp Web抓包:原理、工具与安全”

1. WhatsApp Web的基本原理与架构

WhatsApp Web是WhatsApp的一个网页版应用,它允许用户通过浏览器与手机端的WhatsApp进行同步,实现信息的发送与接收。其基本原理主要依赖于二维码扫描和端到端加密技术。

首先,WhatsApp Web的工作流程始于用户在浏览器中访问WhatsApp Web的官方网站。用户会看到一个二维码,接下来需要用手机上的WhatsApp应用进行扫描。通过扫描二维码,手机端的WhatsApp应用与网页版建立了一个安全的连接。这个二维码实际上包含了一个临时的会话令牌,确保只有经过授权的设备才能访问用户的消息。

WhatsApp Web的架构基于客户端-服务器模型。用户的手机是主要的客户端,负责处理消息的接收和发送,而浏览器则充当另一个客户端。两者之间通过WebSocket协议进行实时通信。WebSocket是一种在单个TCP连接上进行全双工通信的协议,允许数据在客户端与服务器之间快速传输。这种设计的优势在于可以实现即时消息推送,确保用户在网页版上能够及时接收到来自手机端的消息。

在安全性方面,WhatsApp Web采用了端到端加密技术。这意味着消息在发送时会被加密,只有发送者和接收者能够解密查看。这种加密机制确保了即使数据在传输过程中被截获,第三方也无法读取消息内容。此外,WhatsApp Web的连接是基于HTTPS协议的,进一步增强了数据传输的安全性。

除了消息的发送与接收,WhatsApp Web还支持多种功能,包括查看联系人、发送图片和文件、以及进行语音和视频通话等。这些功能的实现同样依赖于与手机端的实时同步,确保用户在不同设备上获得一致的使用体验。

总的来说,WhatsApp Web通过二维码扫描实现设备间的快速连接,利用WebSocket协议实现实时数据传输,并通过端到端加密保障消息的安全性。这种设计不仅提升了用户体验,也确保了用户的隐私安全,为现代通讯方式提供了一个创新的解决方案。

2. 抓包工具的选择与使用方法

抓包工具的选择与使用方法是进行WhatsApp Web分析的关键步骤。首先,常用的抓包工具有Fiddler、Charles Proxy和Wireshark等,这些工具能够帮助用户捕获网络请求并分析数据。 Sniffmaster作为一款全平台抓包工具,支持HTTPS、TCP和UDP协议,可在iOS/Android/Mac/Windows设备上实现无需代理、越狱或root的抓包操作,特别适合移动应用如WhatsApp Web的分析。

在选择工具时,用户需考虑其操作系统的兼容性以及工具的功能特点。例如,Fiddler和Charles Proxy对HTTP/HTTPS流量的支持非常好,适合初学者使用,而Wireshark则适合需要深入分析网络流量的用户,Sniffmaster好上手。

在实际使用过程中,用户需要先配置代理,确保抓包工具能够捕获到WhatsApp Web的流量。在此过程中,注意要能上外网,以便能够顺利连接到WhatsApp的服务器。安装和配置工具后,用户可以通过浏览器访问WhatsApp Web,抓包工具将实时显示网络请求和响应数据,用户可以根据需要分析特定的请求,查看其中的参数和数据内容。这对理解WhatsApp Web的工作原理以及数据传输方式非常有帮助。在进行数据分析时,用户还需关注隐私安全问题,确保不泄露个人信息或敏感数据。在抓包过程中,建议保持对数据的保密性,避免将抓取的数据公开或分享给不信任的第三方。

3. 数据分析与隐私安全的考虑

在进行WhatsApp Web的抓包分析时,数据的隐私安全问题是一个不可忽视的重要方面。随着网络安全威胁的不断增加,用户的个人信息和通信内容面临着潜在的风险。因此,在进行数据分析时,我们需要充分考虑以下几个方面。

首先,抓包过程中获取的数据通常包含用户的消息内容、联系人信息以及媒体文件等敏感数据。这些数据如果落入不法分子之手,可能会导致用户隐私泄露、身份盗窃等严重后果。因此,在使用抓包工具时,务必遵循相关法律法规,确保只在合法范围内进行数据分析,并且要获得相关人员的同意。

其次,进行数据分析时,使用的工具和方法也需谨慎选择。当前市场上存在多种抓包工具,如Fiddler、Charles等,这些工具虽功能强大,但若不加以妥善使用,可能会引发安全隐患。例如,某些抓包工具可能会在未授权的情况下存储用户数据,或是通过不安全的网络传输数据,从而导致数据被窃取。因此,选择可靠的工具,并定期更新其安全补丁,是保护数据安全的有效措施。

然后,在数据分析过程中,建议对敏感信息进行脱敏处理。通过对数据进行加密、匿名化或是只提取必要信息,可以在一定程度上降低数据泄露的风险。同时,分析结果应仅限于内部使用,避免将敏感数据以任何形式公开或分享,确保用户隐私不被侵犯。

此外,用户自身在使用WhatsApp Web时,也应加强安全意识。定期更新密码、启用双重身份验证、避免在公共网络环境下使用WhatsApp Web等都是提升个人信息安全的有效手段。用户应时刻保持警惕,关注账户的异常活动,一旦发现可疑行为,应立即采取措施保护自己的账户安全。

最后,数据分析的结果应有助于提升WhatsApp Web的安全性。通过对抓取的数据进行深入分析,可以识别潜在的安全漏洞和风险点,从而为开发者提供改进产品安全性的依据。这不仅有助于保护用户的隐私安全,也能增强用户对平台的信任,推动整个社交网络环境的健康发展。

综上所述,在进行WhatsApp Web的抓包与数据分析时,隐私安全问题不容忽视。通过合法合规的方式、选择合适的工具、进行数据脱敏处理以及增强用户安全意识,我们能够在享受便捷通信服务的同时,有效保护个人隐私与信息安全。

移动应用上架到应用商店的完整指南:原理与详细步骤

随着智能手机的普及,移动应用程序(App)已经成为人们日常生活中必不可少的一部分。而将自己的App上架到应用商店则是许多开发者的梦想,因为这意味着他们的作品可以被更多人看到、下载和使用。本文将介绍App上架到应用商店的原理和详细步骤。

一、App上架的原理

App上架到应用商店的原理可以简单概括为:开发者将开发好的App上传到应用商店,应用商店审核通过后将App发布到应用商店。在这个过程中,开发者需要遵守应用商店的规定和要求,以确保App能够通过审核并成功上架。

具体来说,开发者需要准备好以下内容:

  1. 应用商店账号:开发者需要在目标应用商店注册一个账号,并遵守该应用商店的规定和要求。

  2. App信息:开发者需要提供App的名称、描述、图标、版本号、支持的设备类型等信息。

  3. App安装包:开发者需要将App打包成符合应用商店要求的安装包,并上传到应用商店。对于iOS应用,可以使用AppUploader等工具在Windows、Linux或Mac系统中上传IPA文件到App Store,无需Mac电脑即可操作,比传统方法更高效。

  4. 证书和签名:开发者需要使用证书和签名对App进行加密和验证,以确保App的安全性和可靠性。使用工具如AppUploader可以简化iOS证书的申请和签名过程,支持多电脑协同,无需钥匙串助手。

  5. 测试和调试:开发者需要对App进行测试和调试,以确保App的质量和稳定性。

二、App上架的详细步骤

  1. 注册应用商店账号

开发者需要在目标应用商店注册一个账号,以便上传App和管理App的信息。不同的应用商店可能有不同的注册流程和要求,开发者需要仔细阅读应用商店的注册指南,并提供必要的信息和证明文件。

  1. 准备App信息

开发者需要准备好App的名称、描述、图标、版本号、支持的设备类型等信息。这些信息将在应用商店中展示,并影响用户对App的印象和选择。

  1. 打包App安装包

开发者需要将App打包成符合应用商店要求的安装包,并上传到应用商店。不同的应用商店可能有不同的安装包要求,开发者需要仔细阅读应用商店的指南,并使用合适的工具和方法进行打包。AppUploader支持快速上传IPA文件,并内置工具查看和编辑相关文件内容。

  1. 证书和签名

开发者需要使用证书和签名对App进行加密和验证,以确保App的安全性和可靠性。证书和签名的获取和使用也可能因应用商店的不同而有所差异,开发者需要仔细阅读应用商店的指南,并按照要求进行操作。利用AppUploader,开发者可以直接创建和管理iOS证书,简化流程。

  1. 测试和调试

开发者需要对App进行测试和调试,以确保App的质量和稳定性。测试和调试的过程可能会涉及多个设备和操作系统,开发者需要尽可能模拟用户的使用场景,并记录和解决问题。AppUploader提供USB和二维码安装测试功能,方便在iOS设备上验证应用。

  1. 提交审核

开发者需要将准备好的App信息、安装包、证书和签名上传到应用商店,并提交审核。审核的过程可能需要几天甚至几周的时间,开发者需要耐心等待,并及时响应应用商店的反馈和要求。

  1. 上架发布

审核通过后,应用商店会将App发布到应用商店,供用户下载和使用。开发者需要及时更新App的信息和版本,并处理用户的反馈和问题。

总之,将App上架到应用商店需要开发者投入大量时间和精力,需要遵守应用商店的规定和要求,并保证App的质量和安全性。只有经过认真准备和审核,才能让自己的App在应用商店中脱颖而出,成为用户喜爱的产品。

从爆红到被嫌弃,MCP 为什么开始失宠了

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优化和协作功能的实现等核心难点。

如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。

如果你对 AI全栈 感兴趣,也欢迎添加我微信,我拉你进交流群

MCP 出生时,被捧得很高。

2024 年 11 月,Anthropic 发布"模型上下文协议",几乎所有 AI 开发者社区都在讨论这件事。它的定位很诱人,要成为大模型和外部工具之间通信的"通用标准",有点像当年 HTTP 对 Web 的意义。一时间,MCP server 满天飞,各种集成教程、开源实现层出不穷。

但时间只过了一年多。

上周,Perplexity 的联合创始人兼 CTO Denis Yarats 在内部表示,他们正在放弃 MCP,转而改用 APICLI。这个消息扩散出来后,引发了一波讨论,但讨论的内容不是"为什么",而是"早该如此"。

Y Combinator 的总裁兼 CEO Garry Tan 甚至直接说了一句话:"MCP sucks。"

MCP 的问题从来都不是技术实现不够好

很多人对 MCP 的质疑,停留在"不稳定"、"认证烦"这些体感上的抱怨。这些问题确实存在,但它们只是表象。MCP 真正的困境,是一个结构性问题。

MCP 的工作方式是,把工具的名称、描述、参数结构(Schema)以及使用示例,全部注入到 Agent 的上下文窗口里。Agent 读完这些信息,再决定要调用哪个工具。

这个设计在工具数量少时还可以接受。但你一旦接入 10 个服务,每个服务有 5 个工具,光是工具定义本身就已经烧掉了几千个 token。Agent 还没开始干活,上下文就已经塞满了一半。

上下文窗口是 Agent 最宝贵的资源,它决定了 Agent 能看见多少对话历史,能保留多少工作记忆,能有多大的推理空间。MCP 的代价,是把这个资源拿来"列菜单"。

面对这个问题,现有的出路只有三条:

  • 一次性加载所有工具,接受推理性能下降
  • 限制接入工具数量,接受 Agent 能力边界收窄
  • 构建动态工具加载机制,接受额外的延迟和复杂度

三条路都不好走。这不是"实现质量"的问题,而是协议设计本身的代价。

除此之外,日常使用中的痛点也不少。MCP server 启动失败是家常便饭,有时重试能解决,有时必须推倒重来。接入多个服务就要在每个服务上重新认证一遍。权限管理也只有"允许"和"不允许"两档,没有办法把某个工具限制为只读,也没有办法约束它可以传什么参数。

CLI 是更好的答案,不是因为它新,而是因为它够旧

工程师 Eric Holmes 写过一篇文章,观点直接:MCP 没有带来任何实际价值,LLM 完全可以自己搞懂怎么用 CLI

这话有点刺,但它说的是实情。

大模型在训练时看过海量的 man 手册、Stack Overflow 回答和 GitHub 上的 Shell 脚本。它们对 CLI 的理解,远比对某个 MCP server 的理解深得多。给它一个命令行工具和一份文档,它就能上手,不需要特殊适配。

CLI 在几个关键点上,比 MCP 天然占优。

第一是可调试性。当 Claude 对 Jira 执行了一个出乎意料的操作,你可以直接跑同一条 jira issue view 命令,看看它看到了什么。输入一致,输出一致,没有谜团。但 MCP 的调用只发生在 LLM 的对话内部,出问题了只能去翻复杂的 JSON 传输日志。

第二是可组合性。这是 CLI 的核心竞争力。你可以用 jq 过滤数据,用 grep 串联逻辑,把输出重定向到文件。这不只是方便,很多时候这是唯一可行的路。MCP 没有这个能力,你要么把完整数据塞进上下文,要么在 server 端自己写过滤逻辑,两种方式都在用更多的精力换取更差的结果。

第三是认证。CLI 复用的是系统级别的认证体系,这套东西已经经过几十年的打磨。MCP 需要你重新为每个工具搭一遍认证流程。

这件事说明了什么

Perplexity 放弃 MCP,以及其他工具陆续移除 MCP 支持,这件事背后有一个更值得思考的信号。

给 AI 构建工具链,不需要发明一套新的协议。AI 需要的工具,和人类需要的工具,在很多时候是同一套。最好的工具是对人类和机器都好用的工具。

CLI 存在了几十年,设计上一直遵循一个哲学,每个工具做好一件事,然后把工具组合起来解决复杂问题。这套哲学放到 Agent 身上,依然成立。

MCP 想构建一个更"现代"的抽象层,但它解决的问题,现有工具已经解决得够好了。在不需要额外抽象的地方强行加一层,带来的只有额外的成本和复杂度。

当然,MCP 不会完全消失。在某些特定场景,比如需要强类型 Schema、有严格访问控制要求的企业内部系统,它依然有它的位置。但作为"AI 工具集成的通用标准",这个定位恐怕很难站稳了。

参考:

充值成功,腾讯成为OpenClaw官方赞肋商

大家好,我是凌览。

如果本文能给你提供启发或帮助,欢迎动动小手指,一键三连(点赞评论转发),给我一些支持和鼓励谢谢。


腾讯和 OpenClaw(网友口中的"龙虾")刚因数据问题针锋相对,转眼便握手言和——这场反转,全程只花了3天。

image.png

事情还得从3月12日说起。OpenClaw创始人Peter Steinberger(PSPDFKit创始人,现已加入OpenAI)当天突然在社交平台"开火",称腾讯未经沟通便抓取了ClawHub技能库1.3万多个数据,直接让服务器成本从几百美元暴涨到五位数(万美元级别)。他原话是"抄袭却不支持项目",还提到收到了腾讯关联方抱怨速率限制的邮件。

腾讯当日即回应:SkillHub是"面向中国用户的本地镜像站",已标注数据来源;上线首周处理用户流量180GB(87万次下载),但从官方源仅拉取1GB数据,实际是为OpenClaw减负。腾讯还强调团队成员本就是OpenClaw代码贡献者,希望未来成为生态赞助商。

戏剧性转折出现在3月15日:Peter直接转发了腾讯成为OpenClaw社区赞助商的博文,配文"喜欢一场精彩的救赎"。至此,腾讯轻量云已正式入驻GitHub赞助名单,与OpenAI、百度同列。

image 1.png

最绝的是这反转速度——3天前还在撕,3天后腾讯就上榜了。Peter这句love a good redemption arc翻译过来就是:腾讯的钞能力,真香👏🏻👏🏻👏🏻

莫名联想到《西虹市首富》:"你看人真准~"哈哈哈

643ea878ec654b16d26b79b54c886d13.gif

Cursor 独有的 12 个技巧:这些是 Claude Code 没有的

前言

Hi~大家好呀,我是清汤饺子。

最近身边问 Cursor 和 Claude Code 区别的人越来越多了。每次被问到,我都会先问一句:你更享受在终端里敲命令,还是更喜欢在编辑器里直接改代码?

这个问题没有标准答案。但如果你选了 Cursor,这篇文章就是为你准备的——那些它独有的、编辑器才能做到的能力。

说起来,我之前写过两篇 Cursor 相关的文章:

这篇只讲一件事:在上面两篇都没覆盖的地方,Cursor 还有哪些独有的能力。那些因为它是编辑器、而不是命令行工具,才能做到的事。


第一部分:编辑器独有的执行能力

1. Plan Mode——计划是可以编辑的

这个功能上一篇文章讲过了,这里不重复啦。只补充一个很多朋友可能没注意到的细节:

Cursor 的 Plan Mode,生成的计划是一个可以直接编辑的文档,而不只是一段可以阅读的文字。

我第一次用的时候还不知道这个功能,后来发现了这个"隐藏技能",才发现它有多好用~

💡 心得:直接在计划上删删改改,比跟 AI 来来回回对话舒服多了。你试一次就知道啦!


2. Debug Mode——有系统地找根因,而不是猜

遇到难搞的 bug 时,你们有没有试过把报错贴给 Agent,让它猜着改?

反正我试过,而且踩过大坑……

有一次我被一个奇怪的 bug 折磨了整整一天,把报错贴给 Agent,它猜着改,一遍不行,再来一遍,还不行……前前后后改了十几个地方,最后发现根因根本不是我最初想的那个问题。

后来我发现了 Cursor 的 Debug Mode,按 Shift+Tab 切换。我跟你说,这个功能简直打开了新世界的大门!

这个功能是怎么工作的呢?

  • 第一步,生成假设:Agent 会先分析你的代码,列出几种可能的根因。它不是直接改,而是先猜一猜可能是什么问题。

  • 第二步,插入日志:它在关键的地方帮你加上 log 语句,这些日志会发送到 Cursor 扩展内置的一个本地 debug server。

  • 第三步,让你复现:Agent 会告诉你具体怎么操作才能触发这个 bug,等你来帮它复现问题。

  • 第四步,分析日志:拿到日志后,Agent 会读取运行时的数据,基于真实的证据来确定到底哪里出了问题。

  • 第五步,精准修复:找到根因后,Agent 只会改最少的代码,往往就几行,不像以前那样改一大堆。

  • 第六步,清理日志:修复完成后,Agent 会自动把之前加的 log 语句全部删掉,不让你的代码变得脏脏的~

💡 心得:Debug Mode 让 Agent 先问"是什么",再回答"怎么修"。这个顺序的改变,把我十几轮猜测压缩成两三次操作。你们也试试~

3. 内置浏览器——Agent 能直接操作你的页面

前端问题用文字描述给 Agent,往往说不清楚对吧?

Cursor 内置了浏览器工具,Agent 可以直接打开你的页面来操作和观察,不需要你手动截图、复制日志~是不是很方便!

Agent 能帮你做这些事

  • 点点点:打开网页、点击按钮、填表单、触发各种用户操作,Agent 都能帮你做
  • 截图:实时看到页面长什么样,布局对不对、样式有没有问题,一目了然
  • 看 Console:JS 报错和调试输出,Agent 直接帮你读,不用你复制粘贴
  • 看 Network:API 调了什么、请求体是什么、返回了什么状态码,一清二楚
  • 可视化改样式:直接在浏览器里拖拖拽拽调整布局、颜色、间距、圆角,确认效果后一键让 Agent 把改动写回代码

怎么用:直接在 Agent 对话里跟它说就行,比如:

@browser 帮我打开 localhost:3000,看看控制台有没有报错,
顺便截个图给我看看登录页现在长啥样

💡 一个小细节:浏览器日志不是直接塞进上下文的,而是先写到文件里,Agent 需要的时候再 grep 读取需要的那几行。这样就算页面输出一大堆噪音,主对话也不会被撑爆~是不是很贴心!

💡 这是 Claude Code 做不到的哦。Claude Code 没有内置浏览器,你只能手动截图或者复制粘贴日志贴给它。Cursor 的浏览器和 Agent 深度集成,前端调试效率差距真的特别明显。你们有同感吗?


4. Parallel Agents + Best-of-N——让多个模型同时跑同一个任务

这是我觉得 Cursor 里最有意思的功能之一,但知道的人不多~

简单说:Cursor 用 Git Worktree 来隔离并行 Agent 的工作区,每个 Agent 跑在独立的分支里,互不干扰,改完可以单独 Apply 到你的主分支。

有两种用法

用法一:普通并行——同时跑多个不同任务,互不阻塞。一个 Agent 在重构支付模块,另一个在写测试,两个任务并行推进,都完成后分别 Apply。是不是很爽!

用法二:Best-of-N——把同一个任务同时交给多个模型,然后挑最好的结果。在 Agent 输入框下方的下拉里选多个模型,提交后你会看到多张卡片,每张对应一个模型的实现方案,点哪里就预览哪里的改动,最后选你最满意的那个 Apply。简直选择困难症的福音!

什么时候用 Best-of-N 呢

  • 遇到很难的问题,不确定哪个模型会给出更好的方案
  • 想看看不同模型在你代码库上表现怎么样
  • 做架构决策的时候,想对比两种不同的实现思路

💡 心得:我现在遇到比较难的任务,习惯性地开 Best-of-N 跑两个模型。不同模型确实会走不同的路,有时候一个完全卡住的问题,换个模型反而一下子就解决了。


第二部分:模型与工作流控制

5. 按任务阶段切换模型,平衡质效

用模型选择器下拉,或按 Cmd+/ 循环切换~

我是这么用的

  • 探索和读代码的时候:用快模型,比如 Gemini Flash 或 Claude Haiku,速度快,还省钱
  • 写代码的时候:换更强的模型,比如 Claude Opus 或 GPT-5,效果更好

💡 一个很关键的点:切换模型的时候,上下文完整保留,不需要重开对话。你可以在同一个对话里,探索阶段用快模型,遇到真正卡住的问题再切到最强模型,完全不影响连续性。这个你们一定要试试!真的好用!

💡 省钱小技巧:在 Cursor Settings → Models 里设置你的日常默认模型。日常开发大部分任务用中等模型就够,把强模型留给真正需要它的场景~很划算!


6. Skills——把最佳实践打包成可调用的命令

上一篇文章里讲过 Rules,Rules 是给 Agent 的持久化背景知识。Skills 是另一个层次的东西:它是把一套完整的操作流程打包成一个可复用、可分享的命令~

简单说:Rules 告诉 Agent "怎么做事",Skills 告诉 Agent "做哪件事"。是不是很好理解!

Skills 放在哪里呢

位置 谁能用到
.cursor/skills/ 或 .agents/skills/ 当前项目
~/.cursor/skills/ 你的所有项目

每个 Skill 是一个文件夹,里面放一个 SKILL.md,还可以附带 scripts/、references/、assets/ 目录~

一个典型的 Skill 长这样

---
name: rework-commits
description: 把当前 branch 的改动重新拆分成语义化的小 commit,每个 commit 只做一件事。提交 PR 前使用。
---

# Rework Commits

## 步骤

1.  git log 查看当前 branch 上的所有改动
2. 按功能边界把改动分组
3.  git add -p 分批暂存,逐个提交
4. 每条 commit message 格式:<type>: <描述>,并说明改动原因

两种调用方式

  • 在 Agent 对话里输入 /rework-commits,直接触发
  • Agent 根据上下文自动判断是否相关,主动调用(在 description 里写清楚"什么时候用",Agent 就能自己判断)

如果不想让 Agent 自动触发,只想手动调用,在 frontmatter 里加一行:

---
name: rework-commits
description: ...
disable-model-invocation: true
---

💡 心得:我把部署前检查、生成 changelog、整理 commit、跑安全扫描这几个流程都做成了 Skills,团队成员 clone 项目就能直接用,不用每次口头交代步骤。Skills 存进 git,团队共享——这是 Rules 做不到的事。


7. Hooks——在 Agent 操作前后插入你自己的逻辑

第一篇文章里提过 Hooks 的基本概念啦~这里不重复基础,重点讲三个 Cursor 独有的能力~

能力一:Tab 专属 Hooks

Cursor 有两套 Hook 事件,分别针对 Agent 和 Tab(内联补全):

Hook 什么时候触发
beforeTabFileRead Tab 补全要读文件之前
afterTabFileEdit Tab 补全写完文件之后

这两个 Hook 只有 Cursor 有哦!它让你对 Tab 自动补全的行为做单独控制,比如在 Tab 读取文件前过滤掉敏感字段,或者在 Tab 写完代码后自动跑格式化。Claude Code 没有内联补全,自然也没有这两个 Hook~是不是很酷!

能力二:团队云端分发

企业版可以在 Cursor Dashboard 里直接配置 Hooks,自动同步到所有团队成员,不需要每个人手动配置或通过 git 分发:

Dashboard → Team Content → Hooks
→ 新建 Hook 配置
→ 自动同步到所有成员(每 30 分钟一次)

配合操作系统定向投放,可以给 Mac 和 Windows 用户分别下发不同的 Hook 脚本。

能力三:开箱即用的合作伙伴集成

Cursor 官方和一批安全工具做了 Hooks 集成,拿来即用:

工具 能帮我们做什么
Semgrep Agent 写完代码自动扫描漏洞,有问题直接让它重新生成
Snyk 实时检测 Agent 操作里的 prompt injection 和危险工具调用
Endor Labs Agent 安装包之前扫描恶意依赖,防供应链攻击
1Password Shell 命令执行前验证 .env 是否正确挂载,密钥不落磁盘

💡 我是这么用的:在 afterFileEdit 里挂一个格式化脚本,Agent 每次改完文件自动跑 prettier,再也不用手动格式化了。团队里用企业版的朋友可以直接从 Dashboard 统一推下去,一次配置全员生效。


第三部分:安全边界

8. 用 .cursorignore 保护敏感文件

默认情况下,Agent 可以读取你项目里的所有文件。如果项目里有密钥、证书、敏感配置,需要主动排除~

这个真的很重要很重要!之前有朋友把 .env 文件忘了加 ignore,结果 Agent 读取了还贴到对话里,尴尬死了……你们千万别学他!

在项目根目录创建 .cursorignore:

# 环境变量和密钥
.env
.env.*
*.key
*.pem
**/secrets.json
**/credentials.json

# 不需要 AI 读取的大型生成文件
dist/
build/*.min.js

💡 全局 ignore:在 Cursor Settings 里可以配置全局 ignore 规则,对你所有项目生效,不用每个项目单独配~


9. 用 sandbox.json 精确管控网络和文件权限

Cursor Agent 在沙盒环境里执行终端命令,你可以通过 .cursor/sandbox.json 精确控制它能访问哪些域名和文件路径~

一个典型的配置

{
  "networkPolicy": {
    "default": "deny",
    "allow": [
      "registry.npmjs.org",
      "pypi.org",
      "*.githubusercontent.com"
    ]
  }
}

default: deny 表示除了明确允许的域名,其他全部拒绝。这是最安全的做法~

还能控制文件系统访问范围

{
  "networkPolicy": {
    "default": "deny",
    "allow": ["registry.npmjs.org"]
  },
  "additionalReadonlyPaths": [
    "/opt/shared/design-tokens"
  ],
  "enableSharedBuildCache": true
}

💡 沙盒内置的保护机制,默认就已经生效,不需要额外配置啦:

  • 私有 IP(RFC 1918)和云服务元数据端点(169.254.169.254)默认全部拒绝,防止 SSRF
  • .cursorignore、.git/config、.vscode/ 等配置文件默认写保护,Agent 改不了
  • Agent 的文件操作默认限制在 workspace 内,不会跑到外面去创建或修改文件
  • 用户级(~/.cursor/sandbox.json)和项目级(.cursor/sandbox.json)可以同时存在,项目级优先,但团队管理员和 Cursor 硬编码的安全规则永远在最上层,任何配置都无法覆盖

💡 有了这些保护,用 Cursor 是不是安心多了!


第四部分:代码质量与团队协作

10. Bugbot——把质量检查从"手动触发"变成"自动运转"

如果说 Plan Mode 是你主动触发的防线,Bugbot 是全自动的后台守卫。你推 PR,它自动跑,在代码行内直接留评论~是不是很酷!

你们敢信?我上次忘了开 Bugbot,结果有个安全漏洞直接被忽略了……后来想想都后怕!

后来开了 Bugbot 之后,每次 PR 它都会自动检查,简直是安全感拉满!

它能帮我们发现这些问题

  • 逻辑错误
  • 空指针风险
  • Race condition
  • 安全隐患
  • 缺失的错误处理

两种触发方式

  1. 每次推 PR 更新,自动运行
  2. 在 PR 下面评论 cursor review 或 bugbot run,手动触发

发现问题后有两种处理方式

  • 点 "Fix in Cursor" 链接,跳回编辑器,让本地 Agent 修复
  • 如果开启了 Autofix,Bugbot 直接调起 Cloud Agent 自动提交修复,全程不需要你介入

还能配置项目特有的检查规则:在 .cursor/BUGBOT.md 里写:

## 安全规范
所有 API 入参必须经过 zod 校验,发现缺失的校验直接标记为 blocking bug。

## 测试要求
修改 backend/ 下的文件必须有对应的测试变更,否则标记为 blocking bug。

团队管理员还可以在 Dashboard 里配置全团队通用的 Bugbot Rules,自动应用到所有仓库,不需要每个项目单独写~是不是很方便!

💡 怎么设置:打开 cursor.com/dashboard → Integrations → 连接 GitHub → Bugbot tab 开启对应的仓库


11. Cloud Agent——把耗时任务放到后台跑

本地 Agent 跑任务时,你需要保持电脑开着,网络连着。如果是个耗时很长的任务,或者你想同时干别的事,用 Cloud Agent~是不是很爽!

我最近天天用 Cloud Agent 写测试覆盖率你们知道吗?以前得开着电脑等半天,现在直接交给 Cloud Agent,跑完自动开 PR,我回来看结果就行,效率拉满!真的!

怎么用:在 Agent 输入框下方的下拉菜单里选 "Cloud",然后正常发送任务~

Cloud Agent 会帮你做这些

  1. 自动 clone 你的仓库到独立的云端沙盒
  2. 在隔离环境里跑任务(能构建、跑测试、操作浏览器)
  3. 完成后开一个 PR,等你来 review
  4. 通过 Slack、邮件或网页界面通知你

还能从这些地方触发 Cloud Agent

从哪里触发 怎么用
cursor.com/agents 网页界面,手机也能用
Slack @cursor [描述],支持指定仓库和模型
Linear 把 issue 指派给 Cursor,或评论 @cursor
GitHub 在 PR 或 Issue 下评论 @cursor [描述]
API 用 API 触发,接入内部工具

💡 Slack 有个很好用的小细节:Cloud Agent 会读取整个 thread 的上下文。团队在 Slack 里讨论了一个问题,直接在那个 thread 里 @cursor 触发,它能理解整个讨论背景,不用你重新解释一遍~很贴心!

💡 关于费用:Cloud Agent 按 API 用量计费,使用前需要在 Dashboard → Settings 里开启 usage-based pricing~很灵活!


12. Automations——定时或事件驱动,让 Agent 全自动运转

这是前两篇都没讲到的功能,也是我觉得 Cursor 最被低估的能力之一~

Cloud Agent 是你主动触发的。Automations 是让 Agent 自己跑起来——不需要你在场,不需要你手动触发。

一句话解释:给 Cloud Agent 配一个触发器,满足条件就自动跑,跑完自动发 PR 或发 Slack 通知~

支持这些触发方式

触发方式 举个例子
定时触发 每天凌晨 2 点、每小时、每周一
GitHub 事件 PR 打开、PR 合并、push 到分支、CI 完成
Slack 事件 某个频道有新消息、消息包含特定关键词
Linear 事件 issue 创建、状态变更、cycle 结束
PagerDuty 事件 告警触发、告警解除
Webhook 自定义 HTTP 端点,接入任何内部系统

举几个真实场景

场景一:每天自动清理 feature flags

触发器:每天凌晨 2 点
任务:扫描代码库,找出已经全量的 feature flags,
 删除相关条件分支,提交 PR

场景二:PR 打开时自动做安全扫描

触发器:GitHub - Pull request opened
任务:分析 PR diff,检查是否有安全漏洞、
 硬编码的密钥、不安全的依赖,
 在 PR 下留 review 评论

场景三:Slack 报 bug 自动触发修复

触发器:Slack - #bugs 频道有新消息
任务:读取消息内容,定位相关代码,
 提交修复 PR,回复到原 thread

Automations 还有记忆功能:同一个 Automation 的多次运行之间可以共享持久化笔记,Agent 能记住上次发现了什么、做了什么,下次运行时接着来。比如安全扫描的 Automation 可以记录"上周已经报告过这个问题",避免重复留评论~是不是很智能!

💡 心得:我现在有三个 Automation 一直在后台跑:每周一自动扫描依赖更新、每次 PR 合并后自动更新 changelog、每天检查 TODO 注释是否有对应的 issue。这些事情以前要么靠人记着做,要么靠 CI 脚本维护——现在交给 Automation,我完全不用操心了~

写在最后

16 + 7 + 12 = 35 个技巧,说多不多,说少不少。

但最重要的是:看完一定要去用哦~

不然就白看啦~

回头看这三篇文章,其实有一条主线贯穿始终:你给 Agent 的上下文质量,决定了它输出的质量。 Rules 是上下文,Plan 是上下文,运行时日志是上下文,浏览器截图是上下文。所有的技巧,本质上都是在帮你把正确的信息送到模型面前。

希望这篇文章对你们有帮助呀~ 有问题欢迎来问我!

欢迎关注我的公众号「清汤饺子」,获取更多技术干货~

qrcode_for_gh_ca5e8769bdf3_1280.jpg

参考文档

零成本本地大模型!用 Next.js + Ollama + Qwen3 打造流式聊天应用

大家好!今天我来给大家分享一个非常实用的技术实现:如何在本地零成本搭建一个可以流式输出的 AI 聊天应用。不需要昂贵的 API 密钥,也不需要复杂的配置,跟着我一步步来,你也能拥有属于自己的本地 AI 助手!

一、先看效果

最终我们能实现这样一个功能:

  • 在本地运行大模型(Qwen3:4B)
  • Next.js 作为后端服务,实现流式转发
  • 前端实时展示 AI 的响应,打字机效果拉满

stream.png

二、准备工作

1. 安装并启动 Ollama

Ollama 是一个非常优秀的本地大模型运行工具,支持各种主流开源模型。

下载安装: 访问 Ollama 官网 下载对应系统的安装包,Windows/macOS/Linux 都支持。

验证安装: 安装完成后,打开终端运行:

ollama --version

如果看到版本号,说明安装成功啦!

2. 下载 Qwen3:4B 模型

Qwen 是阿里开源的系列模型,Qwen3:4B 体积小、速度快,非常适合在普通电脑上运行。

在终端中运行:

ollama pull qwen3:4b

等待下载完成后,我们可以测试一下:

ollama run qwen3:4b

如果能正常和 AI 对话,说明模型已经准备好了!按 Ctrl+C 退出。

三、Next.js 项目搭建

如果你还没有 Next.js 项目,可以快速创建一个:

npx create-next-app@latest my-ai-app
cd my-ai-app

四、核心实现:API 路由开发

这是最关键的一步,我们需要创建一个 Next.js API 路由来对接 Ollama,并实现流式输出。

app/api/ollama/route.ts 中:

import { NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  try {
    const { prompt, model = 'qwen3:4b' } = await request.json()

    if (!prompt) {
      return new Response('Prompt is required', { status: 400 })
    }

    const ollamaResponse = await fetch('http://localhost:11434/api/generate', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model,
        prompt,
        stream: true,
      }),
    })

    if (!ollamaResponse.ok) {
      return new Response('Failed to connect to Ollama', { status: 500 })
    }

    const encoder = new TextEncoder()
    const decoder = new TextDecoder()
    const stream = new ReadableStream({
      async start(controller) {
        const reader = ollamaResponse.body?.getReader()
        if (!reader) {
          controller.close()
          return
        }

        try {
          while (true) {
            const { done, value } = await reader.read()
            if (done) break
            
            const chunk = decoder.decode(value, { stream: true })
            const lines = chunk.split('\n').filter(line => line.trim())
            
            for (const line of lines) {
              try {
                const data = JSON.parse(line)
                if (data.response) {
                  controller.enqueue(encoder.encode(data.response))
                }
                if (data.done) {
                  break
                }
              } catch {
                continue
              }
            }
          }
        } finally {
          reader.releaseLock()
          controller.close()
        }
      },
    })

    return new Response(stream, {
      headers: {
        'Content-Type': 'text/plain; charset=utf-8',
        'Transfer-Encoding': 'chunked',
      },
    })
  } catch (error) {
    console.error('Ollama API error:', error)
    return new Response('Internal server error', { status: 500 })
  }
}

技术要点解析:

  1. 流式转发的核心:使用 ReadableStream 创建自定义流式响应
  2. 数据解析:Ollama 返回的是每行一个 JSON 对象,我们逐行解析并提取 response 字段
  3. Transfer-Encoding: chunked:这个响应头告诉浏览器这是一个分块传输的流式响应

五、前端实现:自定义 Hook + 流式展示

为了代码的复用性和可维护性,我们把流式处理逻辑封装成一个自定义 Hook。

1. 创建 useOllamaStream.ts

'use client'

import { useState, useCallback, useRef } from 'react'

interface UseOllamaStreamOptions {
  onChunk?: (chunk: string) => void
  onError?: (error: Error) => void
  onComplete?: () => void
}

export function useOllamaStream(options: UseOllamaStreamOptions = {}) {
  const [response, setResponse] = useState('')
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const abortControllerRef = useRef<AbortController | null>(null)

  const cancel = useCallback(() => {
    if (abortControllerRef.current) {
      abortControllerRef.current.abort()
      abortControllerRef.current = null
    }
  }, [])

  const sendMessage = useCallback(async (prompt: string) => {
    if (!prompt.trim()) return

    setIsLoading(true)
    setResponse('')
    setError(null)
    
    const controller = new AbortController()
    abortControllerRef.current = controller

    try {
      const res = await fetch('/api/ollama', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ prompt }),
        signal: controller.signal,
      })

      if (!res.ok) {
        throw new Error(`HTTP error! status: ${res.status}`)
      }

      const reader = res.body?.getReader()
      const decoder = new TextDecoder()

      if (reader) {
        while (true) {
          const { done, value } = await reader.read()
          if (done) break
          
          const chunk = decoder.decode(value)
          setResponse(prev => prev + chunk)
          options.onChunk?.(chunk)
        }
      }
      
      options.onComplete?.()
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') {
        setError('Request cancelled')
      } else {
        const errorMessage = err instanceof Error ? err.message : 'An error occurred'
        setError(errorMessage)
        options.onError?.(err instanceof Error ? err : new Error(errorMessage))
      }
    } finally {
      setIsLoading(false)
      abortControllerRef.current = null
    }
  }, [options])

  return {
    response,
    isLoading,
    error,
    sendMessage,
    cancel,
  }
}

2. 创建主页面 page.tsx

'use client'

import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react'
import { useOllamaStream } from './useOllamaStream'

const ResponseDisplay = React.memo(({ response, isLoading }: { response: string; isLoading: boolean }) => {
  const responseRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (responseRef.current) {
      responseRef.current.scrollTop = responseRef.current.scrollHeight
    }
  }, [response])

  if (!response && !isLoading) return null

  return (
    <div style={{ 
      border: '1px solid #ccc', 
      padding: '15px', 
      borderRadius: '5px',
      maxHeight: '400px',
      overflowY: 'auto',
    }} ref={responseRef}>
      <h3 style={{ marginTop: 0 }}>Response:</h3>
      <div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
        {response}
        {isLoading && <span style={{ opacity: 0.5 }}></span>}
      </div>
    </div>
  )
})

ResponseDisplay.displayName = 'ResponseDisplay'

export default function Page() {
  const [prompt, setPrompt] = useState('')

  const { response, isLoading, error, sendMessage, cancel } = useOllamaStream()

  const handleSubmit = useCallback((e: React.FormEvent) => {
    e.preventDefault()
    if (isLoading) {
      cancel()
    } else {
      sendMessage(prompt)
    }
  }, [prompt, isLoading, sendMessage, cancel])

  const containerStyle = useMemo(() => ({
    maxWidth: '800px',
    margin: '0 auto',
    padding: '20px',
  }), [])

  const textareaStyle = useMemo(() => ({
    width: '100%',
    minHeight: '100px',
    padding: '10px',
    marginBottom: '10px',
    fontSize: '16px',
    resize: 'vertical' as const,
  }), [])

  const buttonStyle = useMemo(() => ({
    padding: '10px 20px',
    fontSize: '16px',
    cursor: isLoading ? 'not-allowed' : 'pointer',
    backgroundColor: isLoading ? '#ff4444' : '#0070f3',
    color: 'white',
    border: 'none',
    borderRadius: '5px',
    marginRight: '10px',
  }), [isLoading])

  return (
    <div style={containerStyle}>
      <h1>Hello InstantMind</h1>
      
      {error && (
        <div style={{ 
          backgroundColor: '#ffebee', 
          color: '#c62828', 
          padding: '10px', 
          borderRadius: '5px', 
          marginBottom: '15px' 
        }}>
          Error: {error}
        </div>
      )}
      
      <form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
        <textarea
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder="Enter your prompt here..."
          style={textareaStyle}
          disabled={isLoading}
        />
        <div>
          <button
            type="submit"
            style={buttonStyle}
          >
            {isLoading ? 'Cancel' : 'Send'}
          </button>
        </div>
      </form>
      
      <ResponseDisplay response={response} isLoading={isLoading} />
    </div>
  )
}

六、性能优化亮点

  1. useCallback 缓存函数:避免不必要的函数重新创建
  2. useMemo 缓存样式:样式对象每次渲染都是新的,用 useMemo 可以避免子组件不必要的重渲染
  3. React.memo 缓存子组件ResponseDisplay 组件只有在 responseisLoading 变化时才重新渲染
  4. AbortController 取消请求:支持中途取消生成,体验更好
  5. 自动滚动到底部:内容过长时自动跟随

七、运行项目

启动 Next.js 开发服务器:

npm run dev

然后访问 http://localhost:3000/instamind,输入问题试试看!

八、常见问题

Q: Ollama 连接失败怎么办? A: 确保 Ollama 服务正在运行,检查 http://localhost:11434 是否可以访问。

Q: 响应速度很慢? A: 可以尝试更小的模型,比如 qwen3:1.8b,或者升级电脑硬件。

Q: 可以更换其他模型吗? A: 当然!在 API 路由中修改 model 参数即可,Ollama 支持的模型都可以用。

总结

今天我们实现了:

  1. ✅ 本地 Ollama 服务搭建
  2. ✅ Qwen3 模型下载和运行
  3. ✅ Next.js API 路由流式转发
  4. ✅ 前端流式响应展示
  5. ✅ 请求取消、自动滚动等优化

这一套方案完全免费,所有数据都在本地,隐私性极佳!快去试试吧!

如果你觉得这篇文章对你有帮助,别忘了点赞、收藏、关注三连!有问题也可以在评论区交流~


📦 完整代码

本博客对应的代码已发布 v0.0.1 版本:

👉 GitHub Release - v0.0.1

参考资料:

2026 年前端 Agent 框架选型:Mastra 与 LangChain 该怎么选

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优化和协作功能的实现等核心难点。

如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。

如果你对 AI全栈 感兴趣,也欢迎添加我微信,我拉你进交流群

2026 年的前端圈卷出了新高度,AI Agent 已是各类 Web 应用的标配。官网智能客服、内部任务助手、产品内的搜索与推荐,都绕不开一件事:用哪个框架把大模型和工具串起来。不少团队会在 LangChain.js 和 Mastra 之间反复纠结,架构评审时也常为此争论。

两者没有绝对优劣,差别主要在"设计哲学"和"业务场景"的匹配度。Mastra 像为前端量身定制的厨师刀,刀刃顺手、切菜切肉都轻松;LangChain.js(尤其是 LangGraph.js)则像重型瑞士军刀,刀锯镊子开瓶器齐全,能应付各种复杂场景,代价是重量和复杂度都更高。下文从 2026 年技术生态出发做一次对比,并配上代码与图示,方便你理清思路、少走弯路。

为什么前端选型会卡在这两个框架上

前端接大模型、做 Agent,本质是三件事:把用户输入送给 LLM、根据输出决定下一步(是否调工具、是否多轮对话)、再把结果还给用户。不同框架在这三条链路上的抽象程度和侧重点差异很大。一类把"循环调工具、拼消息"全包在内部,对外只暴露"发消息、拿回复",你几乎不用关心内部调了几轮工具;另一类把节点、边、状态都暴露给你,自己搭图,灵活性高,但概念和代码量都上去,得先建立"图"的思维才能写得顺手。

LangChain 从 Python 生态长出来,后有 langchain-js,再后来复杂编排催生了 LangGraph,面向任意语言和部署环境的通用编排,概念多、集成广,前端只是消费端之一。文档里会反复出现 Runnables、LCEL、RunnableSequence、RunnablePassthrough 以及各种 @langchain/xxx 包,学习路径会先经过"什么是 Runnable、reducer、checkpointer"这一串概念。Mastra 则从 TypeScript 和现代前端框架出发,默认你在用 Next.js、Nuxt 等全栈框架,API 和类型系统都围着前端习惯转,包名和概念更收敛,文档集中在"在 React、Server Actions 里怎么用",很少逼你先学一整套编排术语。

选型归根结底就两点:团队和产品更接近"通用 AI 编排"还是"前端优先的轻量 Agent"。前者偏向后端或全栈做复杂系统,愿意为灵活性和生态付学习成本;后者偏向前端或小团队在现有 Web 应用里快速接一层智能,希望少概念、少依赖、快上线。

如下图所示。

20260314114857

两种哲学一目了然:一侧是前端优先的轻量体验,一侧是通用编排与生态。

Mastra 的定位与优势

Mastra 从诞生起就面向 TypeScript 和现代前端框架(Next.js、Nuxt 等),针对前端痛点,主打开发者体验(DX)。

TypeScript 原生与类型安全

端到端类型推导做得很好:Agent 的输入、输出和工具调用参数在 IDE 里都有完整类型提示和自动补全,不必手写类型转换或 as 断言。工具用 zod 或 TypeScript 类型定义入参,框架自动生成模型可用的 schema 并做运行时校验。例如在 createTool 里写 inputSchema: z.object({ location: z.string() }),调用时入参即被推断为 { location: string },返回值与 outputSchema 对齐,和现有基于 zod 的表单校验、API 契约也容易打通。

轻量且贴合全栈框架

部署在 Vercel、Cloudflare Workers 等 Serverless 或 Edge 上时,Mastra 的冷启动和边缘兼容性通常更好。没有 LangChain 那套 Runnables、LCEL 等抽象层,依赖树干净,打包体积可控,不必为"跑通一个带工具的 Agent"拉满 @langchain/core@langchain/openai@langchain/langgraph 等一长串包。在 Next.js 的 Server Action、Route Handler 里直接调 Mastra Agent,心智负担小,和现有数据流(表单、状态、API)易对齐,也方便和 React Server Components、流式 SSR 配合。

心智负担低

API 贴近前端数据流直觉:发一段消息、拿一段回复、必要时调几个工具。Mastra 把 LLM 调度、工具解析和流式输出包起来,用简单异步函数或 React 友好接口暴露,不必理解"图、节点、条件边、reducer",会写 createToolnew Agent、会调 generate 或流式方法就能跑通,适合作为团队第一个 Agent 项目的起点。

适合的场景小结

Mastra 特别适合这几类情况:

  • Agent 主要是 Web 应用的辅助功能(智能搜索、客服助手、简单数据总结或表单建议),且深度绑定 Next.js、React 生态。
  • 团队以前端或全栈为主,不想引入过重后台架构,希望快速迭代上线,同时要类型安全和良好调试体验。
  • 对依赖体积、冷启动和 Edge 兼容性敏感,不想为用不到的能力背上整座 LangChain 生态。

LangChain.js 与 LangGraph 的定位与优势

到 2026 年,单纯用 LangChain 搞复杂 Agent 已不够用,实际在评估的往往是 LangGraph.js,它是处理复杂、有状态、多 Agent 协作时的常用方案。

生态系统覆盖广

冷门向量库、大模型厂商、各种外部 API,LangChain 生态里大多已有现成集成。Pinecone、Weaviate、Qdrant、Chroma、自建 REST、OpenAI、Anthropic、Cohere、国产大模型,以及 Tavily、SerpAPI 等,多数有官方或社区的 @langchain/xxx 包。公司内有老旧系统、私有模型或特定协议时,也容易在现有集成上做薄封装,复用 LangChain 的 Runnable、消息格式和工具约定,快速对接大量外部依赖时能省下不少适配和调试时间。

状态机与图逻辑(LangGraph)

需要"循环思考、多路分支、人类介入(Human-in-the-loop)"的复杂工作流时,LangGraph 的图架构能精确控制节点流转。节点是处理单元(一次 LLM 调用、工具执行或人工审核),边是状态转移(固定边或条件边)。状态可持久化到 checkpointer,刷新或断线重连后从断点继续,适合多轮任务和多人协作,也是 Mastra 目前不直接提供的部分。

过度抽象的代价

学习曲线陡:Runnables、Chains、Tools、Nodes、Edges、Annotation、reducer、checkpointer 等概念交织,新手易迷路。实现"用户问一句、模型调一次工具再回答"这种简单功能,也要先理解状态结构、写 agent 与 tools 节点、配条件边和普通边再 compile,代码量明显多于"Agent 配置 + 一次 generate"。报错常来自链式调用的某一层,堆栈里是 LangChain 内部的 Runnable 名,前端背景的开发者需要时间习惯"从图的角度想问题"。LangGraph 的 TypeScript 类型虽完整,但状态是运行时用 Annotation 和 reducer 拼出来的,和 Mastra 那种"工具入参即 zod schema、一眼能看出类型"的体验比,心智负担更大。

适合的场景小结

LangChain、LangGraph 更适合这几类情况:

  • 核心业务就是复杂 AI 系统:多 Agent 协作、长时运行异步任务、或需精准控制"思考中断与恢复"。
  • 集成需求多且杂,要接内部老旧系统或非常小众的向量库、模型接口。
  • 要对底层 Prompt、重试、记忆(Memory)做深度定制,甚至改框架默认行为。

核心能力对比

用一张表概括两个方向在关键维度上的差异,细节在前后文展开。

维度 Mastra LangChain.js / LangGraph
设计核心 极致 DX、原生 TS、轻量化 复杂编排、状态管理、大生态
学习曲线 平缓,熟悉 TS 即可快速上手 陡峭,需理解大量框架专属概念
调试体验 堆栈清晰,贴合前端习惯 多层抽象,报错有时难以定位
多 Agent 支持,更适合简单链式交互 极强,循环与状态打断控制完善
生态与集成 精选集成,覆盖主流工具 海量集成,几乎覆盖常见基础设施
依赖与体积 包少、体积小,Edge 友好 多包组合,体积与冷启动略大

Mastra 通常只需 @mastra/core 加模型适配(如 OpenAI),LangChain 则常需 @langchain/core@langchain/openai(或其它模型包)、@langchain/langgraph,再接向量库或 RAG 还会多几个包,在 Serverless 冷启动和 Edge 里更敏感一些。

如下图所示。

20260314115005

从设计重心到依赖体积,一张图能看清两边差异。下面用两段代码对比同一需求的实现方式,再给出选型决策说明。

用代码感受两种 API 风格

同一需求"做一个能查天气的对话 Agent",在 Mastra 和 LangGraph 里写出来的代码量和抽象层次差很多,看一遍再想选型会直观不少。

Mastra:工具 + Agent 几行搞定

在 Mastra 里用 createTool 定义工具的入参(zod)、描述和执行函数,创建 Agent 时把工具挂上去即可。调用时直接 agent.generate() 或流式接口,不用关心"模型要不要调工具、调完要不要再推理",框架内部处理。

下面示例定义了一个查天气工具和一个使用该工具的 Agent。工具入参用 z.object 声明,execute 的返回值与 outputSchema 一致,整条链路在 IDE 里都有类型推导。示例使用 OpenAI 当前主力模型 gpt-5.4,实际项目里可通过环境变量配置 API Key。

import { createTool } from "@mastra/core/tools";
import { Agent } from "@mastra/core/agent";
import { z } from "zod";

const getWeather = createTool({
  id: "get_weather",
  description: "根据城市名称查询当前天气,适合回答天气相关提问",
  inputSchema: z.object({
    location: z.string().describe("城市名称,如北京、上海"),
  }),
  outputSchema: z.object({ summary: z.string(), temp: z.number().optional() }),
  execute: async ({ location }) => {
    // 实际项目里这里调和风、OpenWeather 等 API
    return { summary: `${location} 晴`, temp: 22 };
  },
});

const weatherAgent = new Agent({
  id: "weather-agent",
  name: "天气助手",
  instructions: "你是天气助手,用 get_weather 查天气并简洁回复用户。",
  model: "openai/gpt-5.4",
  tools: { getWeather },
});

// 在 Next.js Route Handler 或 Server Action 里直接调用
const result = await weatherAgent.generate("北京今天天气怎么样?");
console.log(result.text);

在 Next.js 的 Route Handler 里暴露成 API 时,导入 weatherAgent,对请求体里的消息调 generate 或流式方法即可,不必再写状态机或图。

LangGraph:显式建图与状态

在 LangGraph 里,要先定义状态结构(例如消息列表)、再定义"agent"节点(调用模型、可能产生 tool_calls)和"tools"节点(执行工具并返回 ToolMessage),最后用边把节点串起来,并加上"是否继续调工具"的条件边。模型用 LangChain 的 ChatOpenAI 接 OpenAI 最新模型,工具用 bindTools 绑定,循环由图的拓扑自然形成。

下面这段示例用 StateGraph 定义了一个单 Agent、带一个天气工具的最小图。状态里只有 messages,agent 节点读最后一条用户消息并调用模型,若返回 tool_calls 则路由到 tools 节点,执行完再回到 agent,直到模型不再调工具为止。可与上面 Mastra 示例对照,体会"图"和"状态"的显式写法。

import { StateGraph, Annotation, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { HumanMessage, AIMessage, BaseMessage } from "@langchain/core/messages";
import { ToolNode } from "@langchain/langgraph/prebuilt";

const model = new ChatOpenAI({
  model: "gpt-5.4",
  apiKey: process.env.OPENAI_API_KEY,
});

const getWeather = tool(
  async (input: { location: string }) => `${input.location} 晴,22℃`,
  {
    name: "get_weather",
    description: "根据城市名称查询当前天气",
    schema: z.object({ location: z.string() }),
  },
);

const modelWithTools = model.bindTools([getWeather]);
const toolNode = new ToolNode([getWeather]);

const AgentState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (left, right) => left.concat(right),
    default: () => [],
  }),
});

async function agentNode(state: typeof AgentState.State) {
  const response = await modelWithTools.invoke(state.messages);
  return { messages: [response as AIMessage] };
}

function shouldContinue(state: typeof AgentState.State): "tools" | "end" {
  const last = state.messages[state.messages.length - 1] as AIMessage;
  return last.tool_calls?.length ? "tools" : "end";
}

const graph = new StateGraph(AgentState)
  .addNode("agent", agentNode)
  .addNode("tools", toolNode)
  .addEdge("tools", "agent")
  .addConditionalEdges("agent", shouldContinue, { tools: "tools", end: END })
  .compile();

const result = await graph.invoke({
  messages: [new HumanMessage("北京今天天气怎么样?")],
});
console.log(result.messages[result.messages.length - 1]);

同样实现"用户问天气、模型调工具、再回复":Mastra 是"Agent + tools 配置 + 一次 generate",LangGraph 是"状态注解 + 两节点 + 条件边 + compile"。前者适合快速落地和前端集成,后者适合加人工审核、多 Agent 分支、断点续跑等复杂控制。示例中 LangGraph 使用 gpt-5.4,API Key 建议从环境变量 OPENAI_API_KEY 读取。

选型决策思路

可以按"产品形态、团队基因、集成与定制需求"三条线问自己,再对照上文对比。决策主线就一条:先看 Agent 是"应用的核心"还是"应用里的辅助能力"。核心场景(多 Agent、长任务、状态中断与恢复、大量冷门集成或深度定制)更倾向 LangGraph;辅助能力(Next/React 为主、快速迭代、极重 TypeScript 与 DX)更倾向 Mastra。不必二选一,也可以简单对话用 Mastra、复杂管线用 LangGraph,按模块边界拆。

如下图所示。

20260314115134

从"核心还是辅助"出发,到倾向 LangGraph 或 Mastra(或两者组合)的决策路径。

更偏向选 Mastra 的情况

  • 产品形态上,Agent 主要作为 Web 应用的辅助功能(智能搜索、客服助手、简单数据总结等),且深度绑定 Next.js、React 生态。
  • 团队以前端、全栈为主,不想引入过重的后台架构,希望快速迭代、快速上线。
  • 你非常看重 TypeScript 的类型安全和开发体验,对臃肿依赖和难以排查的报错比较排斥。

更偏向选 LangChain / LangGraph 的情况

  • 产品形态上,核心业务就是一个复杂的 AI 系统,例如多 Agent 协作、长时间运行的异步任务、或需要精准控制思考中断与恢复。
  • 集成需求多且杂,需要连接内部各种老旧系统,或使用非常小众的向量数据库、模型接口。
  • 需要对底层 Prompt、重试、Memory 等做深度定制,甚至改动框架默认行为。

结合业务场景做更细的取舍

光看框架特性不够,最终要落到"这个 Agent 具体负责什么"上。下面四类典型场景方便对号入座,每类对应不同复杂度和集成需求,选错框架要么大材小用,要么后期自己造轮子。

如下图所示。

20260314115312

四类场景与推荐方向的对应关系。

场景一:官网或产品里的智能客服、搜索建议

用户在一页里问几句,要即时、简洁的回复,必要时查文档或知识库。流程短、状态简单,不需要多 Agent 博弈或断点续跑,前端发一条消息、收一条(或流式)回复,至多一两轮工具调用。这类需求 Mastra 的轻量 API 和 TypeScript 体验很顺手,一个 Agent 配几个工具、在 Route Handler 里调 generate 就能上线;用 LangGraph 容易杀鸡用牛刀,要先建图、理解条件边和状态,对只想做一个会查文档的客服的团队来说性价比不高。

场景二:内部工具里的"多步任务助手"

例如用自然语言帮用户订会议室、填工单、查数据并生成报告。步骤多,有时要人工确认或回退(如"是否确认提交工单"),状态要在多轮请求间保持,甚至支持"离开页面再回来从断点继续"。这类需求用 LangGraph 的状态图和 checkpointer 更自然:节点对应步骤或人工介入,边上挂条件判断,状态持久化后刷新或重连都能恢复。用 Mastra 也能做,但分支和人工介入一多,就得自己维护"当前步骤、待确认项、历史结果",等于在业务层再造状态机,不如直接用图建模,让框架负责持久化。

场景三:多 Agent 协作(检索、生成、审核等分工)

多角色各司其职,之间有固定或动态调用关系,甚至要循环几轮才产出结果。这类编排是 LangGraph 的强项;Mastra 更适合"一个主 Agent 调若干工具"的链式交互,多 Agent 的路由和状态共享要自己写胶水代码。

场景四:向量库、模型、外部 API 集成种类多

公司内有自建向量库、多种大模型和第三方 API,希望用同一套抽象管住"检索、调用、解析"。LangChain 的集成生态在这里优势明显:Pinecone、Weaviate、Qdrant、自建 REST、各类 LLM 与 RAG 预制链和图,大多有现成包。Mastra 偏向精选常用组合,技术栈若较"非主流"(内网模型、私有协议、冷门向量库),可能要自己写适配层,把外部能力包成 Mastra 能识别的工具或模型接口。要权衡多写的适配代码是否被 Mastra 的 DX 和轻量部署抵消;若集成种类还会持续增加,直接上 LangChain 生态往往更省事。

常见误区与落地注意点

选型时容易踩的坑和落地前值得想清楚的几点,简单归纳如下。

不必纠结的两点。第一,没有"用了 Mastra 就不能用 LangChain"这回事,两者可共存,例如边缘或 BFF 用 Mastra 做轻量对话,后台用 LangGraph 做复杂管线,用 HTTP 或消息队列打通。第二,没有"LangGraph 一定比 Mastra 重"的绝对结论,重的是你要维护的图与状态逻辑;若你只需要一张简单 agent-tools 图,编译后运行时开销可接受,主要是上手成本高。

需要提前想清楚的两点。一是"先简单后复杂"时,若判断半年内会演进到多 Agent 或人机协同,可早点把复杂子流程用 LangGraph 建模,哪怕先只实现单 Agent,图结构也为后续加节点留好位置,避免以后在 Mastra 里手写状态机再迁一轮。二是"先复杂后简化"时,若团队普遍抱怨 LangChain 报错难查、概念太多,可把"单轮或短对话"抽成独立服务,用 Mastra 重写,接口不变、前端无感,逐步降维护成本。

最后,无论选哪边,都建议一开始就把"输入输出契约"(请求体格式、流式 SSE、错误码)定好,并用 TypeScript 类型或 OpenAPI 描述出来,以后换实现、做 A/B 或拆服务时,前端和网关都不必大动。

混合使用与迁移成本

不少团队会折中:简单、面向用户的 Agent 用 Mastra,部署在前端或边缘;复杂、长链路、多 Agent 的管线放后端,用 LangGraph 或 LangChain 实现,通过 API 暴露。这样既保住前端侧的开发体验和性能,又在需要复杂编排时用上 LangChain 生态。

若一开始选了 Mastra,后面业务演进到必须上状态图、多 Agent,可以只把"复杂子流程"迁到 LangGraph,用 HTTP 或消息队列和现有 Mastra Agent 对接,不必全盘重写。例如前端仍用 Mastra 做即时问答,把多步审批、长任务编排单独做成 LangGraph 服务,Mastra 在需要时调该服务 API。反过来,若一开始用 LangChain 搭了简单客服,发现维护成本高、报错难排查,可以把单轮或短对话抽成独立服务,用 Mastra 重写,逐步迁移。关键是想清楚边界(按功能、按请求路径、按团队 ownership 都行),按边界拆模块,而不是非此即彼。迁移时优先保证输入输出契约稳定(统一 JSON 请求体、流式 SSE 格式),前端或网关就不必大改。

总结与下一步

Mastra 和 LangChain(LangGraph)代表两种设计哲学:前者为前端和 TypeScript 优化,追求轻量和 DX;后者面向通用 AI 编排和复杂状态,追求生态和表达能力。没有谁一定更好,只看和你的业务场景、团队结构、集成与定制需求是否匹配。

一句话记住选型心法:Agent 是"应用里的辅助能力"、团队偏前端、要快上线,优先看 Mastra;Agent 是"业务核心"、有多 Agent、长任务、人机协同或大量冷门集成,优先看 LangGraph。两者也可组合,按模块边界拆,契约定好即可。

建议先明确两件事:当前要做的 Agent 主要负责什么(辅助功能还是核心 AI 系统),以及半年到一年内会不会出现多 Agent、长任务、复杂集成或深度定制。有了这两个问题的答案,再对照文中的对比表、决策说明和四类场景,选型会清晰很多。若你愿意说一下目前在规划的 Agent 具体负责什么业务、会接哪些系统,可以在此基础上再做一轮更细的技术栈评估和落地方案设计。文中的代码示例使用当前主流的 gpt-5.4,可直接复制后按需改模型名和 API Key 配置。

搜索 C++ 引擎回归能力建设:从自测到工程化准出|得物技术

一、为什么要做这件事

在搜索系统中, C++ 引擎长期扮演着底层核心基础设施的角色:性能敏感、逻辑复杂、变更频繁,同时承载着大规模线上流量的稳定运行。随着业务持续发展和技术架构不断演进,我们逐步意识到:在高频迭代背景下,回归能力也需要同步升级。

过去一年,我们围绕搜索 C++ 引擎展开了一次系统性的回归能力工程化建设。本文将介绍这次能力升级的背景思考、核心设计思路以及落地实践。

高频迭代背景下:回归能力需要同步升级

搜索 C++ 引擎的升级主要来自三类需求:业务功能需求、重要技术项目(有 QA 深度参与)、大量技术优化与结构性改造需求。

在实际迭代节奏中,技术优化与结构性改造类需求占比较高,引擎整体呈现出多人并行开发、持续迭代推进的状态。随着规模扩大,我们发现:现有回归环境更适用于单次项目式验证。多需求并行时,资源调度与复用能力仍有提升空间,回归准出标准尚未完全工程化。这意味着,在稳定性要求不断提升的背景下,我们有必要构建更加标准化、流程化的回归体系,让质量保障能力与迭代节奏匹配。

现有测试方式的演进空间

当前搜索引擎主要依赖两类测试手段:DIFF 测试和压测,这些手段在长期实践中发挥了重要作用,但随着业务复杂度提升,我们也逐步看到进一步优化的空间:流量获取依赖下载日志、手工上传,自动化程度仍可提升。DIFF 过程中存在自然噪音。需要更精细化处理(AA DIFF、排序不稳定)。报告与分析信息分散在不同工具中,定位效率有优化空间。多套工具并行使用,缺乏统一平台化沉淀。整体来看,测试能力更多体现为“工具能力集合”,而在流程标准化、资产沉淀与统一治理方面仍有提升空间。

二、我们要解决什么问题

这次建设的目标,并不是简单“再做一个工具”,而是希望系统性解决以下问题:让 DIFF 和压测成为搜索 C++ 引擎的标配回归能力、让回归结果具备可分析、可归因能力、让回归成为发布的硬性准出标准、保证工具本身的稳定性,不成为新风险、整体提升引擎的回归效率和交付质量、通过流程和流水线,降低对“人”的依赖。一句话总结:把回归这件事,从“靠自觉”,变成“靠系统”。

三、整体方案概览

围绕上述目标,我们将建设拆分为五个关键方向:流量录制:一次录制,多处复用。环境建设:稳定、可复用的 DIFF/ 压测环境。DIFF 工具体系:从“能跑”到“好分析”。一键压测能力:降低执行门槛。工具与索引平台集成:让回归真正被用起来。

下面将会按模块展开说明。

流量录制:回归的基础设施

为什么先做流量录制

DIFF 和压测的核心前提只有一个:真实、稳定、可复用的流量。因此我们优先建设了搜索 C++ 引擎的流量录制链路,作为后续所有测试能力的基础。

流量如何触发

  • 索引平台集群详情页直接发起流量录制。
  • 索引平台更新 ARK 配置中心中的录制配置。
  • 搜索 C++ 引擎实时监听配置变化。

录制配置设计

所有配置统一收敛在 dsearch3#test.properties,支持:

  • 全局开关。
  • 指定 app / group。
  • 截止时间。
  • 指定 IP。
  • 采样率(0~100)。

这使得录制行为可控、可回收、可精细化管理。

流量生成与存储

  • 引擎侧根据配置生成 Kafka 消息。
  • 多业务场景复用同一 ARK 集。
  • 多场景流量复用同一个 Kafka Topic。

最终流量落入 ODPS,按天分区,字段包含:

  • 请求体。
  • 流量场景。
  • 实验信息。
  • 环境信息(生产 / 预发)。

这为后续 DIFF、压测、问题复现提供了统一数据源。

流量存储字段说明:

request_type:流量标签(原C++引擎请求类型)
app_name:C++引擎appName
group_name:C++引擎groupName
request_body:录制的C++引擎请求体
env:录制的流量环境:预发/生产
graph_name:图名称
experiments:实验列表(搜索新增)
pt:ODPS分区,按天分

DIFF 测试:从无到“可归因”

DIFF 执行流程:

DIFF 的入口统一在索引平台:查询流量 →选择流量→配置参数→触发 DIFF→查看报告。底层由测试服务 + 脚本完成:流量筛选与改造、请求转发、去噪、报告生成与存储。

DIFF 对比方式:

对照组部署 master 分支,实验组部署预发布分支。指定行或者指定集群方式请求对照组和实验组环境。打开新功能开关进行响应比对,生成预期有DIFF报告。

DIFF 环境设计

支持两种模式:

  • 指定集群:对照组 / 实验组两套完整集群。
  • 指定行 精确绑定 search / rank IP。

通过该设计,保证对比的唯一变量只有代码和配置。

流量筛选与回放改造

支持多维度筛选:

  • 搜索场景(交易 / 社区 / 聚合等)。
  • 流量标签(综合 / 销量 / 新品等)。
  • 实验命中情况。

同时解决了生产流量无法直接在预发回放的问题(表名、图参数、模型等适配)。

DIFF 策略设计

我们不只关注“有没有 DIFF ”,而是关注这个 DIFF 是否符合预期,因此 DIFF 被拆为两类:

响应 DIFF
  • 响应字段对比。
  • 漏斗算子字段对比。
指标 DIFF
  • 相似度分布(忽略/不忽略排序)。
  • 漏斗算子一致率。
  • 字段增删改统计。
  • 定制化指标。

DIFF 去噪

DIFF 不可用,往往不是因为“真问题”,而是噪音太多。我们重点处理了:AA DIFF(排序不稳定、非确定性逻辑)、可忽略字段、数值微小波动、内部超时导致的异常结果,目标只有一个:让开发看到的DIFF,尽可能都是真问题。

DIFF 报告设计

报告展示

DIFF 汇总报告:

  • 应用、集群、请求接口、流量标签、路由信息、对比数量、DIFF 数量、完全一致率、query_tag 平均召回数、score 平均分等。
  • 相似度分布统计报告(不忽略排序/忽略排序)。
  • 漏斗算子一致率统计报告。
  • 字段增删改统计。

DIFF 详情报告:

  • traceId、一致率、增删改字段、请求体等。
  • 漏斗算子 DIFF 明细。
  • 响应 DIFF 明细。
报告通知

通知到群 @个人,添加报告链接。

压测:一键完成性能回归

压测执行流程:

  • 索引平台作为压力测试发起入口,查询流量->选择流量->填写压测参数->压测触发->压测记录查看。
  • 测试服务提供索引平台操作的接口能力,查询流量->流量筛选->压测文件生成->压测任务触发->压测状态更新。
  • 压测平台提供实际压测能力,启动压测任务->生成压测报告。

整个过程无需人工干预。

执行方式:

  • 对照组:master 分支。
  • 实验组:预发布分支。
  • 开启新功能开关。
  • 阶梯式加压,对比性能曲线。

压测环境设计

同 DIFF 环境建设。

压测报告设计

报告展示

压测平台报告。

报告通知

通知到群 @个人,添加报告链接。

发布流水线与准出机制

回归能力建设的最终目标,是进入发布流程。当前已完成:UT / MR 流水线初步建设,后续规划中将:把 DIFF 和压测作为发布硬性卡点、回归不通过,禁止上线、回归过程自动扩缩容,避免长期占用资源、自动生成准出报告。

四、后续规划

回归执行率 100%:解决“忘跑回归”。

准出流水线全自动化。

横向覆盖更多搜索场景(流控、商业化、国际搜索等)。

形成统一的上线 SOP 规范。

五、总结

搜索 C++ 引擎回归能力建设,并不是一次“工具升级”,而是一场工程化治理:把经验变成流程、把自觉变成约束、把风险前移到上线之前,最终目标只有一个:让搜索引擎的每一次升级,都更可控、更可信。

往期回顾

1.得物社区搜推公式融合调参框架-加乘树3.0实战

2.深入剖析Spark UI界面:参数与界面详解|得物技术 

3.Sentinel Java客户端限流原理解析|得物技术

4.社区推荐重排技术:双阶段框架的实践与演进|得物技术

5.Flink ClickHouse Sink:生产级高可用写入方案|得物技术

文 /耿辉

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

iOS App 安全加固流程记录,代码、资源与安装包保护

项目上线前的安全处理,经常被放在发布流程的最后一步。很多团队在代码开发阶段关注功能实现,等到准备提交 App Store 时,才开始思考应用被反编译或资源被提取的问题。

在一个包含 Swift + Flutter 模块的项目中,我们曾经遇到过这样一个情况:测试包被外部获取后,对方直接解压 IPA,通过类名和资源目录快速定位了核心模块。那次经历之后,我们把 iOS app 保护单独整理成一套固定流程,并加入到发布前的检查清单中。

这篇文章按实际操作过程记录一个流程。工具不会只有一个,而是组合使用系统能力、命令行工具以及 Ipa Guard 等二进制处理工具。


一、检查 IPA 内部结构

在进行任何保护操作之前,可以先观察当前 IPA 包含的信息。

.ipa 文件复制一份并改名为 .zip

mv app.ipa app.zip
unzip app.zip

进入目录:

Payload/AppName.app

此时可以看到:

  • 可执行二进制文件
  • 图片资源
  • json 配置
  • HTML / JS
  • Storyboard 或 xib
  • embedded.mobileprovision

如果资源目录中存在明显业务含义的文件,例如:

vip_purchase_bg.png
subscription_config.json
payment_success.html

那么即使没有阅读代码,也能推测应用功能结构。


二、在源码阶段减少符号暴露

在 IPA 层处理之前,可以在 Xcode 构建阶段减少调试信息。

Release 配置中可以检查两个选项:

Strip Debug Symbols During Copy = YES
Deployment Postprocessing = YES

构建完成后,用命令查看二进制中的字符串:

strings AppBinary | grep ViewController

如果能看到大量业务类名,例如 OrderManagerVipViewController,说明符号仍然暴露。

源码阶段可以通过脚本或重命名策略减少可读性,但很多项目已经进入稳定阶段,不希望再修改代码结构。这时可以转向 IPA 级处理。


三、对 IPA 二进制进行符号混淆

在编译完成的情况下,可以通过 Ipa Guard 直接对 IPA 包进行处理,而不需要修改项目源码。

加载 IPA 后,工具会解析其中的 Mach-O 二进制结构,并列出类名与方法列表。

在界面中可以看到类似结构:

代码模块
 ├─ OC 类
 ├─ Swift 类
 ├─ OC 方法
 └─ Swift 方法

加载ipa

实际操作时,我们只勾选包含业务逻辑的类,例如:

OrderManager
VipSubscriptionController
PaymentService

处理后,这些名称会被替换为无意义字符串,从而降低反编译可读性。

Ipa Guard 支持 Objective-C、Swift、Flutter、Unity3D 等多种开发平台,因此混合项目也可以统一处理。


四、处理资源文件结构

代码不是唯一需要保护的内容。资源文件往往更容易暴露信息。

在 Ipa Guard 的资源模块中,可以选择处理以下文件类型:

  • 图片
  • json
  • js
  • html
  • mp3
  • xib
  • storyboard

工具会执行两类操作:

1. 文件名混淆

例如:

vip_background.png

会变为:

a9d3f21.png

这样在解包 IPA 时无法通过名称判断用途。 文件名称

2. 修改 MD5

图片或资源的 MD5 值也可以被修改,这可以打散资源特征值。

处理完成后,重新解压 IPA 可以看到所有资源名称已经变为随机字符串。 md5


五、处理 HTML 与 JS 文件

如果应用包含 H5 页面,需要额外处理 JS 与 HTML 文件。

在构建阶段可以使用前端压缩工具,例如:

terser
uglify-js

压缩完成后再由 Ipa Guard 修改资源名称。

这样做的效果是:

  • 文件内容被压缩
  • 文件名称失去语义

即使解包 IPA,也很难通过资源结构还原功能模块。


六、删除调试信息

很多项目在构建过程中会留下调试日志或符号信息。

Ipa Guard 提供调试信息清理功能,可以删除:

  • 自动注释
  • 调试符号
  • 部分字符串信息

处理后可以再次检查:

strings AppBinary

输出内容会明显减少。


七、重新签名并安装测试

任何 IPA 内容修改都会导致签名失效。

因此混淆完成后需要重新签名。

可以使用签名工具,例如:

kxsign sign my.ipa \
-c cert.p12 \
-p password \
-m dev.mobileprovision \
-z test.ipa \
-i

参数 -i 会尝试直接安装到连接的设备。

也可以使用 Ipa Guard 内置签名模块,在混淆完成后直接选择证书并生成新 IPA。

设备测试阶段主要检查:

  • 页面加载是否正常
  • 动态调用方法是否失效
  • H5 页面是否可以打开
  • 是否出现崩溃日志

八、发布阶段生成最终 IPA

测试通过后,需要重新签名生成发布版本。

发布阶段只需要更换证书:

Distribution Certificate
App Store Provisioning Profile

生成的 IPA 将用于提交 App Store。

发布类型 IPA 不允许直接安装到设备,但可以通过 Xcode Organizer 或上传工具提交审核。


iOS app 保护并不是单一技术,而是一组连续操作:减少符号暴露、混淆代码名称、处理资源文件、清理调试信息、重新签名并验证运行。

参考链接:ipaguard.com/tutorial/zh…

iOS App 性能测试工具怎么选?使用克魔助手(Keymob)结合 Instruments 完成

在移动应用开发中,性能测试不是某个阶段才开始做的事情。很多问题在开发早期就已经发生,只是在功能逐渐增多之后才表现出来。例如:

  • 页面滚动出现卡顿
  • 内存持续增长
  • 启动时间越来越长

如果只依赖单一工具去分析这些问题,往往会比较吃力。实际项目中更常见的做法是多工具组合使用,让每个工具负责不同方面。

这里结合一次真实项目中的测试,介绍一套比较实用的 iOS App 性能测试流程。


性能测试通常关注哪些指标

在开始之前,需要先确定要观察哪些数据。常见的性能指标包括:

  • CPU 使用率
  • 内存占用
  • 帧率(FPS)
  • 网络请求
  • 应用能耗

不同阶段关注的重点会有所不同。开发阶段通常更关注函数级性能,而测试阶段更关注设备整体运行情况。


第一方面,设备本机性能监控

在很多团队里,测试人员并不一定使用 Mac 环境。如果需要在 Windows 或 Linux 上查看设备性能,就需要借助设备监控工具。

我在项目中比较常用的是 克魔助手(Keymob) 来做这方面的数据采集。

它的作用主要是:

  • 查看设备运行时 CPU / 内存 / FPS
  • 指定某个 App 进行监控
  • 记录性能变化趋势

这类监控通常用于快速发现问题出现的时间点。


使用克魔助手监控 App 性能

实际操作过程比较简单。

连接设备

准备一台测试设备,然后:

  1. 使用数据线连接 iPhone
  2. 打开克魔助手
  3. 等待设备识别完成

设备识别后可以看到当前设备信息。


进入性能图表

在左侧导航中选择:

性能图表

这里会显示设备当前的资源使用情况。


选择监控指标

在界面右上角可以选择需要观察的指标,例如:

  • CPU
  • 内存
  • FPS

如果只是测试页面流畅度,通常只需要勾选 CPU 和 FPS。 图表


指定要监控的应用

点击 选择 App

输入应用名称即可找到目标应用。 也可以同时勾选 系统总 CPU,用来判断设备整体负载。 选择app


开始测试

点击 开始 按钮之后,就可以在手机上执行测试流程,例如:

  • 打开首页
  • 滑动列表
  • 进入详情页

性能图表会实时显示资源变化。

通过观察曲线可以判断:

  • 哪个操作触发了 CPU 峰值
  • 是否出现持续高占用

第二层:深入分析工具

设备监控工具只能告诉我们问题出现在哪里,但不能直接解释原因。

当发现异常之后,通常需要回到开发工具进行深入分析。


Instruments

Instruments 是 iOS 官方提供的性能分析工具。

它可以分析:

  • 方法调用耗时
  • 内存分配
  • GPU 渲染
  • 线程状态

例如,当设备监控发现某个操作 CPU 突然升高,可以用 Instruments 再跑一次相同操作。

这样可以找到具体的函数或对象。


一个案例

有一次测试人员反馈:

“进入某个页面之后滑动明显卡顿。”

排查过程是这样的:

第一步

使用克魔助手监控 CPU 与 FPS。

发现滑动列表时 CPU 占用突然升高,同时 FPS 出现下降。


第二步

在 Mac 上使用 Instruments 重新测试。

最终定位到问题原因:

页面滚动时触发了大量图片解码。


第三步

修改代码,将图片解码改为后台线程处理。

再次测试后 CPU 曲线明显平稳。


为什么不建议只依赖一个工具

有些开发者希望找到一个全能工具,但在实际项目中很少存在这种工具。

更合理的方式通常是:

设备监控工具,用于观察设备运行情况

开发分析工具,用于定位具体代码问题

这样可以形成一个完整的测试流程。

性能测试并不是某个阶段才进行的工作,而是贯穿整个开发周期的过程。只要在每个版本发布前进行简单的性能监控,就可以提前发现很多潜在问题。

参考链接:keymob.com/tutorial/zh…

使用 Ipa Guard 命令行版本将 IPA 混淆接入自动化流程

当项目进入稳定迭代阶段,很多团队都会把构建流程放进 CI,例如 Jenkins、GitHub Actions 或 GitLab CI。编译 IPA、运行测试、生成构建产物都可以自动完成。但如果需要在发布前做代码混淆或资源处理,图形界面工具就会显得有些不方便。

我在维护一个长期更新的 iOS 项目时遇到过类似问题:每次构建完成后,都需要对 IPA 进行一次混淆处理。如果完全依赖界面操作,就意味着要人工导入 IPA、选择符号、再导出结果。几次之后就会发现,这一步完全可以放进自动化脚本里。

Ipa Guard 的命令行版本正好适合这种场景。它把 IPA 解析、符号混淆、资源处理这些步骤拆成可以调用的命令,同时还能输出符号映射文件,方便排查崩溃问题。下面记录一套实际操作流程。


一、准备待处理的 IPA

CI 构建完成后会生成一个 Release IPA,例如:

build/game.ipa

这就是后续混淆操作的输入文件。

在开始处理前,可以简单检查一下包内结构:

unzip game.ipa

确认 Payload 中包含应用二进制与资源目录即可。之后重新打包,保持原始 IPA 作为备份。


二、导出可混淆符号列表

Ipa Guard 命令行工具的第一步是解析 IPA,提取可修改符号。

执行命令:

ipaguard_cli parse game.ipa -o sym.json

执行完成后会生成一个 sym.json 文件。

这个文件的作用很直接:列出 IPA 中可以被混淆的符号,例如类名、方法名或变量名,并附带相关引用信息。

打开文件后可以看到类似结构:

{
  "confuse": true,
  "name": "_isPreTTS",
  "refactorName": "_isPreTTS",
  "types": ["oc_method_name"]
}

name 是原始符号名, refactorName 用于填写混淆后的名称。


三、根据项目情况调整符号文件

这一步比较关键,因为它决定哪些符号会被修改。

编辑 sym.json 时需要注意两件事:

1. refactorName 长度要保持一致

某些二进制符号长度变化可能影响结构,因此建议保持长度不变。

例如:

_isPreTTS

可以改为:

_a1b2c3d4

字符数量一致即可。


2. 不适合混淆的符号需要关闭

例如下面这个方法:

addEventListener:

如果 JS 或 H5 模块中通过字符串调用它,修改后可能导致运行失败。

可以把:

"confuse": true

改成:

"confuse": false

sym.json 中的 fileReferences 字段可以帮助判断某个符号是否在脚本或资源文件中被引用。


四、使用符号文件执行混淆

完成符号文件修改后,就可以执行 IPA 混淆。

示例命令:

ipaguard_cli protect game.ipa -c sym.json --image --js -o confused.ipa --email ipaguard@gmail.com

参数含义:

  • -c sym.json 指定符号配置文件
  • --image 修改图片 MD5
  • --js 混淆 JS 资源
  • -o confused.ipa 输出文件
  • --email 登录账号

执行后会生成新的 IPA,例如:

confused.ipa

此时包内的符号和资源已经完成处理。


五、对混淆后的 IPA 进行签名

由于混淆修改了 IPA 内容,原有签名已经失效。

需要重新签名才能安装到设备。

可以使用签名工具,例如 kxsign

kxsign sign confused.ipa \
-c cert.p12 \
-p certpassword \
-m dev.mobileprovision \
-z test.ipa \
-i

参数说明:

  • -c 证书文件
  • -p 证书密码
  • -m 描述文件
  • -z 输出 IPA
  • -i 安装到设备

如果连接了测试手机,命令执行完成后会自动安装。


六、设备测试与崩溃排查

混淆后的版本一定要运行一遍完整流程,例如:

  • 登录
  • 支付
  • 页面加载
  • H5 模块调用

如果发生崩溃,可以借助 Ipa Guard 生成的符号映射文件查找原始函数名。

映射文件会记录:

混淆前符号
混淆后符号

这样在 Crash 日志中看到混淆名称时,仍然可以找到对应代码位置。


七、将混淆步骤接入 CI

当流程稳定后,可以写一个简单脚本:

build ipa
ipaguard_cli parse
edit sym.json
ipaguard_cli protect
kxsign sign

在 Jenkins 或 GitHub Actions 中执行即可。

这样每次构建完成都会自动生成混淆后的 IPA。


八、发布阶段的签名

测试通过后,签名流程保持一致,只需要换成发布证书:

kxsign sign confused.ipa \
-c dist.p12 \
-p certpassword \
-m dist.mobileprovision \
-z release.ipa

发布证书生成的 IPA 无法直接安装,但可以上传 App Store。

如果构建环境是 Linux 或 Windows,也可以使用上传工具完成提交。


结尾

将 IPA 混淆接入自动化流程后,发布过程会变得更稳定。符号解析、混淆处理、资源修改和签名测试都可以通过脚本完成,而不是依赖人工操作。

参考链接:ipaguard.com/tutorial/zh…

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

❌