阅读视图

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

为什么不要让LLM帮我们写文档

这篇文章是今天看到的,觉得还不错,翻译一下。

当我们在写文档或文章时,实际上是在提出问题并回答它。例如,一份产品需求文档(PRD)回答的是“我们要干嘛?”;一份技术规范(Tech Spec)回答的是“我们要怎么做?”。有时,问题本身更难回答——“我们究竟想要达到什么目标?”而每一次尝试回答,其实都是在反思自己是否问对了问题。

但现在,我们有了大语言模型(LLM),由 LLM 生成的文档、文章越来越多,我们需要注意这其中可能有一定的风险:每一份由 LLM 生成的文档,可能都会让我们错失一次思考或者是建立合作信任的机会。

一、让 LLM 替我们写作,就像付费健身

写作的目标不是“写完”,而是“增进理解”——首先是对自己的理解,其次是对周围环境的理解。当我们被委派去写东西时,我们的任务是深入迷雾,去征服未知,带着清晰的结构和洞察走出来。

写作的另一个目标可能是让自己变得更强。这就像健身:每一次对自我极限的挑战,都会促进力量的增长,过程是痛苦且费力的,但是我们能得到正反馈。相反,让 LLM 替我们写作,就像付费健身,虽然很舒适,却无法提升自己的能力(认知或者是写作)。

二、LLM 生成的文字不仅破坏了表达的真实性,也破坏了背后思考的真实性

LLM 生成的文档,还会对我们的公信力产生一定的影响。当我发给别人一份带着明显“AI 味”的文档时,只是在证明 LLM 产出了一些接近他人想听的内容,而不能证明我深入思考过这些东西,这明显不是我的思考产物。

当与别人就这份文档内容进行交流时,我们可能不知所云,这在一定程度上会降低别人对我们的信任,更有甚者,可能因为这份信任的丧失,我们会失去一次机会。

LLM 生成的文字不仅破坏了表达的真实性,也破坏了背后思考的真实性。如果文字是自动生成的,那么观点是否也是自动生成的呢?

三、LLM 在写作过程中的正确用途

LLM 在调研和检查工作时非常有用,它们很擅长快速记录信息或转录文本。

它们尤其擅长生成创意,在这个场景下,它们表现卓越,因为即使生成了 10 个点子而只有一个有用,也无伤大雅。我们可以取其精华,去其糟粕。

因此,可以说,大模型能提高软件交付的效率,但为了最大限度地利用它们,我们必须同时提升我们的思想深度。

参考文献:

alexhwoods.com/dont-let-ai…

为什么生产环境很少手写流式响应:AI SDK 三层架构一次讲清

我们在 AI 应用开发 | 手写流式输出:把打字机效果背后的数据流拆开看 里面,已经把流式输出这件事手写跑通了。

但真把这套东西往聊天应用里接的时候,你很快就会感觉到,问题已经不是字怎么出来了,而是后面那一串和业务本身无关、却又必须有人接住的细节:

chunk 边界、半截 JSON、消息历史怎么记、流到一半要不要让用户停下。

今天聊的是 为什么要从手写切到 Vercel AI SDK

先划重点

手写流式响应跑通之后,往真实聊天应用走,会被两件事情拖住:

一类是看不见的协议翻译,拆 chunk、拼半个字、认结束标记; 另一类是看不见的对话状态,记历史、知道在不在流、能不能重生成。

AI SDK 把这两类工作,分散到三个位置接走:

  • 在最上游,把不同模型的 API 翻成一种统一格式
  • 在服务端,统一调模型 + 统一回一条结构化事件流
  • 在前端,统一维护消息数组和流状态

手写版不是不能跑,是越往前走越像在造基础设施

手写版跑通之后,真接聊天应用你会发现,花时间的地方已经不在打字机效果上了。

一部分时间花在协议边界上。

一个中文字符完全可能被拆到前后两个 chunk 里,一段 JSON 只拿到半截就 JSON.parse 直接炸掉,SSE 事件还得自己按 \n\n 切分、挑出 data 字段、认结束标记。

这些工作原本和业务没一点关系,但不接住一个,整条链就走不通。

chunk 是网络边界,不是业务消息边界。

chunk-vs-message-boundary.png

另一部分时间花在对话状态上。

手写版的前端 state 一开始通常就长这样:

const [answer, setAnswer] = useState('');
const [isStreaming, setIsStreaming] = useState(false);

answer 这个字符串 state 一开始单轮够用,但一旦需求里出现多轮历史、重生成、从中间分叉,每条消息还得分清是用户还是 AI、是文字还是工具调用、有没有完成,这一串东西塞不进一个字符串里。

一段字符串记得住一句话,记不住一段对话。

answer-vs-messages.png

所以手写版的问题不是不能跑,是你一旦从 demo 往真实聊天应用走,就得在协议和状态这两层各造一套自己的基础设施

花时间的地方不再是业务,而是这些重复又容易出错的细节。


AI SDK 覆盖的是整条链,不是某一个点

聊到 Vercel AI SDK 之前,我先说一下我自己对它的认知是怎么变的。

我一开始也以为它就是个 React hook,useChat 一调就完事了。后来多看几次才发现,它其实是一整套工具

从模型怎么接进来、服务端怎么调模型、到前端怎么维护对话状态,整条链都覆盖了。

前面那两层工作,协议翻译状态管理不是某一个文件的问题,是从模型到前端整条链上散落的细节

能把这两层一起接住的,也只能是一套覆盖整条链的东西,而不是一个孤立的 hook 或者一个孤立的服务端函数。

我自己是用一家餐厅在脑子里记这条链的

一家餐厅要上菜,它先得有供应商。

不同供应商送来的东西规格千差万别,有的按斤、有的按箱,包装单据也都不一样。

如果后厨每接一家就要重学一遍人家的规矩,这家餐厅根本开不起来。

所以稍微大一点的餐厅都会有一个收货环节,不管哪家供应商送来的,都按统一的规格入库,后厨拿到的永远是同一种格式。

收货之后,后厨才开始干自己的活,按统一的菜单做菜,按统一的餐具盛出来,再交给前台。后厨不关心这块牛肉是哪家送的,它只认入库后的规格。

前台的工作又是另一回事。它不做菜,但它要记住这一桌点了什么、上到第几道、客人有没有催菜、能不能换菜。

AI SDK 这套架构,几乎就是把这家餐厅的分工原封不动搬过来了。

  • 不同模型厂商就是不同的供应商
  • provider adapter(@ai-sdk/xxx 就是收货环节,把不同厂商 API 的差异在这里一次性抹平
  • streamText + toUIMessageStreamResponse() 就是后厨,统一调模型 + 统一往外回一条结构化事件流
  • useChat 就是前台,记住这桌的整段对话现在是什么状态

ai-sdk-three-layer-chain.png

带着这张图往下读,后面三个节点就是这条链上的三个停靠点。


第一个节点:先把模型接进来

不同模型厂商的 API 本来就未必长一样,路径、鉴权 header、流式事件格式、文本字段路径都可能不一样。

你如果每次都直接对着厂商 API 写代码,很快就会反复写这一家怎么接、那一家怎么接。

这时候 AI SDK 的模型接入层就开始把这事接过去了。

provideradapter 这两个词后面会反复出现,我们先来认识一下。

provider 就是模型的供货方DeepSeek 是一个 provider,OpenAI 是一个 provider,Anthropic 也是一个 provider

adapter 就是替你跟这家 provider 对话的那段代码。AI SDK 的做法是,每家 provider 都对应一个 adapter 包

一个 adapter 收一家 provider

import { createOpenAICompatible } from '@ai-sdk/openai-compatible';

export function createDeepSeekAiSdkProvider() {
return createOpenAICompatible({
name: 'deepseek',
apiKey: process.env.DEEPSEEK_API_KEY!,
baseURL: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com'
});
}

@ai-sdk/openai-compatible@ai-sdk/anthropic 这一类包,做的事不是生成文本。

它先替你把不同模型厂商接成 AI SDK 能认的一种统一模型接口。

adapter 的粒度,不是公司品牌,而是协议形状

这里我一开始也有点疑惑,DeepSeek 是一家独立的大模型公司,为什么要用一个叫 openai-compatible 的包来接?

DeepSeek 的 API 形状和 OpenAI 高度兼容,请求路径、请求体字段、流式事件格式基本一致,所有为 OpenAI 写的工具链几乎零代码就能接入。

这不是 DeepSeek 一家的选择,Moonshot、Groq、OpenRouter、Ollama 走的都是同一条路。

所以 @ai-sdk/openai-compatible 根本不在乎对面是 OpenAI 还是 DeepSeek,它只看协议形状对不对,给它配上 baseURLapiKey 就行。

Claude 是走另一条路的典型。

它有自己一套独立的 API 形状,路径、鉴权 header、事件类型、字段结构都和 OpenAI 不一样,所以要走 @ai-sdk/anthropic 这种专门适配。

这一层先替你接住的,不是页面,而是上游模型 API 的差异。


第二个节点:服务端怎么统一调模型和回流

模型接上之后,前端发一条消息过来,服务端总得先有一层把这次请求接住,拿到消息、调模型、再把结果回给前端

如果你用的是 Next.js,这层通常就在 app/api/.../route.ts 里,最常见就是那个 POST 函数。

后面如果我提到 route handler,你先把它理解成这层服务端入口就行。

原来这层里那一坨读流、缓冲、拼字符串的代码,现在基本就剩调一个函数加返回一个函数。

SDK 版服务端入口还剩什么

import { convertToModelMessages, streamText, type UIMessage } from 'ai';

export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();

const result = streamText({
model: deepseek.chatModel('deepseek-chat'),
messages: await convertToModelMessages(messages)
});

return result.toUIMessageStreamResponse();
}

这几行做了三件事:

convertToModelMessages 把前端消息格式翻成模型能认的格式;

streamText 调模型拿流式结果;

toUIMessageStreamResponse() 再把结果翻回前端能消费的事件流。

原来手写版那一大坨读流、缓冲、拼字符串的代码,基本都被这三个函数接走了。

手写版那层细节,SDK 是怎么处理的

mermaid-sdk.png

请求上游模型读流续字处理事件边界结束标记,这些原来散落在服务端入口这层的工作,

全部被 provider adapter 加 streamText 在内部接走。

最后那条「怎么把结果回给前端」的统一响应,则由 toUIMessageStreamResponse() 接走。

服务端轻下来的不是业务逻辑,是那些重复的流式协议处理。

前后端之间,其实还隔着一道翻译

convertToModelMessagestoUIMessageStreamResponse() 这两个函数,是一对翻译器

前端要渲染工具调用卡片、错误块、思考过程,所以 UIMessage 里带了一堆 parts

但模型只认扁平的 role + content

一端多一端少,中间就得有人翻。

convertToModelMessages把前端消息翻给模型toUIMessageStreamResponse()把模型流再翻回前端消息流

进去翻一次、出来也翻一次。

为什么回流不能只是纯文本

这里我一开始也没想清楚,手写版那套纯文本流明明能 work,toUIMessageStreamResponse() 为什么要做成一串带 type 字段的 JSON 事件?

因为前端要消费的不止是字。

AI 中途要显示「正在调用 search 工具」和工具结果卡片,要把灰色的「思考过程」和最终回答分开。

中途抛错要让 UI 识别这不是回答内容,结束时还要有一个明确信号解锁输入框。

如果流里只有字,浏览器根本没法区分哪段是思考、哪段是工具调用、哪段是错误、哪段是最终回答。

所以 SDK 把回流做成了结构化事件流,每个事件都带 type: "text-delta"type: "tool-call" 这种标签,前端看到什么 type 就走什么分支。

structured-event-stream.png

打开浏览器 DevTools 看一眼真实返回流会更直观:

{"type":"text-delta","id":"txt-0","delta":"Vercel"}
{"type":"text-delta","id":"txt-0","delta":" AI"}
{"type":"text-end","id":"txt-0"}
{"type":"finish-step"}
{"type":"finish","finishReason":"stop"}
[DONE]

模型 原始 SSE 里,文本增量走的是厂商自己的字段路径,结束信号也走的是厂商自己的结束格式。

AI SDK 回给前端的事件流里,文本增量统一变成 text-delta,结束信号统一变成 text-endfinish-stepfinish 这一类标签。

浏览器看到的已经是统一后的结果,不是模型厂商自己的原始协议。

前端处理渲染只要按 type 分发就行,不需要再认任何厂商的方言。

顺带留个钩子:messages 为什么是整段传上来的

你看这层服务端入口里 const { messages } = await req.json(),可能会以为前端传一句话、后端维护历史就行。

大模型 API 本身是无状态的

OpenAI、Claude 等大模型的 HTTP 接口其实都没有会话这个概念,每次请求都得把整段对话历史重新喂一次,模型拿到数组那一刻才「恢复」出上下文,生成完立刻丢掉。

所以这条链上的服务端入口每次都是全新的、无记忆的,它只是个转发层,状态落在前端的 messages 数组里。

对话越来越长之后,这个数组也会越来越大,token 吃不消了怎么办、记忆怎么维护,这是下一篇要讲的事。


第三个节点:前端怎么接住消息状态

服务端这层轻下来以后,前端这层也就没那么复杂了。

手写版前端状态,一开始通常就长这样

const [input, setInput] = useState('');
const [answer, setAnswer] = useState('');
const [isStreaming, setIsStreaming] = useState(false);

SDK 版就会变成:

const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({ api: '/api/ai-sdk-chat' })
});

useChat 暴露的不是 answer,是整个 messages 数组

这两段代码的差别,真不是少写几个 state 这么简单。

useChat 接住的是请求发送、流读取、消息追加、状态流转

你不用再自己读 response.body,也不用自己一边拼字符串,一边维护 isStreaming 什么时候开、什么时候关。

更关键的是它暴露给你的不是一个 answer,是一整个 messages 数组,每条消息长这样:

type UIMessage = {
id: string;
role: 'user' | 'assistant';
parts: Array<{ type: string; text?: string }>;
};

这套结构本来就是给整段对话准备的。

多轮、重生成、从中间分叉这些功能往上一加,全都顺势就成立。

举个例子,regenerate() 就是对 AI 最后一条回答不满意,重新生成一次

手写版里这件事通常不是再发一次请求这么简单,你得先砍掉最后一条 answer、把字符串状态倒推回消息数组、再补重发逻辑。

而 AI SDK 这边,messages 数组本来就记着每条消息,调一个方法的事。

所以真正少的不是代码行数,是你不用自己从零搭一套消息结构。

但页面最后怎么长,还是业务自己决定

它也没有替你把前端全部做完。

输入框 state、消息怎么渲染、错误 UI 怎么展示、按钮什么时候禁用、滚动什么时候到底,这些还是业务自己决定。

比如输入框的 input / setInput 就是业务自己维护的,useChat 不管:

<textarea value={input} onChange={(event) => setInput(event.target.value)} />
<button type="submit" disabled={status !== 'ready'}>
  {status === 'streaming' ? 'AI SDK 流式输出中...' : '调用 AI SDK 流式输出'}
</button>

useChat 接的是聊天状态,不是聊天页面长相。


如果你正从手写版迁过来,先改这三处

前面那条三层链路看懂之后,回到代码里第一刀该下在哪,其实顺序是固定的。

第一刀,先改 provider。

先别动前端,用 @ai-sdk/openai-compatible 或对应的 adapter 包把厂商 API 包一层,让后面 streamText 能直接调,上游差异先在这里隔离掉。

第二刀,再改服务端入口。

把手写读流、缓冲、组装响应那层换成 streamTexttoUIMessageStreamResponse()服务端重复的协议细节先拿掉

第三刀,最后改前端状态。

answer 这种单字符串状态换成 useChatmessages后面多轮、重生成、分叉、工具调用才有稳定地基

从手写版切到 AI SDK,第一刀先别改页面,先改协议层和状态层。

migration-three-cuts.png

改完这三刀,你会在两件事上感觉到变化。

一是原来服务端那坨 chunk 拼接、JSON 兜底、字段路径的代码,不用再看第二眼,协议那层不用碰了

二是后面再冒出「重生成」「从某条消息重开」这类需求的时候,你不用先回头重构 state,useChat 暴露的 messages 数组已经为这些需求打好地基了。

省下来的不只是代码行数,更像是脑子不用再同时装怎么读流怎么记对话这两件事。


现在来想想以下问题

Q1:服务端返回了一个 type: "tool-call" 的事件,但前端不知道怎么渲染。你觉得问题出在三层里的哪一层?

💡 provider adapter 负责接模型,streamText 负责调模型和回流,useChat 负责前端状态。tool-call 事件已经到了前端,说明前两层没问题。

Q2:如果你要给聊天页面加一个「从这条消息重新开始」的功能,手写版和 SDK 版各需要改什么?

💡 手写版的 answer 是一段字符串,你得先重构出消息数组才能定位到"这条消息"。SDK 版的 messages 数组里每条消息都有 id,天然支持这个操作。

Q3:现在 useChat 每次请求都把整个 messages 数组传给服务端,对话聊了 50 轮之后,你觉得会先撞到什么问题?

💡 这是下一篇的主题:对话越来越长,token 吃不消了,记忆怎么维护。


感谢您的阅读~🌹

我在微信公众号 前端Fusion 中也会持续同步更新关于 AI 与前端开发的相关文章,欢迎大家关注,一起交流学习。

分享底图_压缩.png

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

导读 introduction

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

01 长程任务的特征

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

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

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

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

02 关注的点

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

效果。

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

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

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

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

速度。

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

成本。

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

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

03 困难在哪里

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

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

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

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

04 核心原则

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

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

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

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

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

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

05 理念

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

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

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

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

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

图片

△ 不同任务边界的实现

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

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

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

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

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

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

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

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

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

06 技巧

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

6.1 任务粒度

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

图片

△ dispatch调度架构图

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

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

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

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

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

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

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


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

6.3 File As Progress

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

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

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

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

6.4 任务状态设计

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

TODO → IN_PROGRESS → DONE
                   → FAILED
                   → SKIPPED


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

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

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


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

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

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

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

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

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

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

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

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

6.5 多轮重试

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

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

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

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

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

图片

△ 多轮重试分层

07 示例

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

7.1 全量 Code Review

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

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

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

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

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

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

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

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

7.2 JS to TS 迁移

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

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

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

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

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

08 从经验到框架:Skill for Skill

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

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


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

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

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

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

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


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

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


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

09 结尾

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

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

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

复刻字节 AI 开发流:实践 Node.js 通用脚手架

前言

最近与前同事深入交流,他现为字节某组的 TL(组长),团队规模近 10 人。在讨论他们团队的 AI 工作流实践中,也得到一些八卦信息。

第一个是: 人才储备变化

他们团队已基本停止招聘实习生,今年仅招一人 个人解读: AI 协作模式下,新人的边际效应大幅下降

第二个是:组织预期转变

业内多位跟他同级的 Leader 私下评估:今年可能出现大规模调整 个人解读: 生成式 AI 在降本增效上的潜力被普遍看好,提前做好裁员准备

第三个是:开发方法论的周期性

  • 层出不穷的新概念:Vibe Coding、SDD 规范、Harness Engineering……
  • 理性看待:这些都是过渡阶段产物,随着大模型升级,这些方法论的生命周期可能只有几个月 个人建议: 不必投入过多精力去追赶,因为等你熟练一种方法论后,可能用不了多久就被淘汰了

第四个是:招聘标准演变 前端岗位招聘标准很多 JD 上开始冠以"全栈开发" 的名义招聘了 实际面试:侧重前端能力考察,但开始要求一些后端基础能力了 个人解读: AI 协作工具提效下,企业希望前端承担更多职能

好了,八卦之后,关于他们的 ai 工作流的核心如下:

你得教 AI,不断沉淀并固化它的规范

具体实践方法:

  • 识别问题:第一次遇到某类问题,AI 执行效果不理想
  • 人工介入:手动处理并解决问题
  • 规则沉淀:将解决方案固化为规则或 Skill
  • 迭代积累:问题解决越多,效率提升越明显,也就是后期 ai 执行的只要不是新的场景和复杂业务,基本不会出错

本质: 通过持续的反馈循环来训练和优化 AI 的工作能力,而不是一开始就寻求完美的工作流

这个理念在字节技术专家杨晨的全软软件开发大会分享中也提出过:Prompt = 可训练资产(像模型一样优化)

让后我个人抽象了这个单 Agent 的工作流程图如下:

image.png

接下来我会解释每个步骤的思路和如何落地:

步骤 1:项目初始化 + 全局规则设计

项目初始化是指,大多数现代框架都提供了开箱即用的脚手架工具,例如:

# Vue/React 生态
npm create vite@latest

# Nest.js
nest new project-name

# Hono.js
pnpm create hono@latest

初始化完成后,立即在根目录创建规则文件,通常命名为 CLAUDE.md 或 AGENTS.md。

这个文件是 AI 协作的宪法,应包含例如项目定位,技术栈清单,核心哲学(例如测试先行,采用 TDD 测试方案),项目结构示例,AI 协作原则等等内容。有兴趣的同学可以找我要这个项目的全局规则。

两个关键注意事项动态迭代 规则文件不是一成不变的。当发现 AI 写的代码不规范时,要想到如何抽离抽象的规则来约束,而不是一味手动修改

SKILL 机制(规则的模块化) 当规则内容过多时,抽象可复用的 Skill 文件。好处是 AI 按需加载,不会每次都把全局规则加入上下文,大大减少 token 用量

规则中引用 SKILL 的示例:

## 3. 核心哲学:测试先行 (TDD)

参考 `.trae/skills/tdd-first/SKILL.md` 中的测试驱动开发规范。

所有新功能开发或 Bug 修复必须遵循 **「红-绿-重构」** 循环。**严禁**在没有对应测试用例的情况下提交业务逻辑代码。
---

## 4. 响应格式

参考 `.trae/skills/response-standard/SKILL.md` 中的响应格式规范。

**[强制]** 所有接口统一返回 JSON,使用 `@/utils/response` 中的工具函数生成响应。

---

步骤 2:需求分析

如果你很清楚自己要做什么,可以直接下发任务。但如果只有模糊想法,建议先和 AI 一起做需求分析。

推荐提示词

你好!现在的任务是:我们要从零开始设计并实现 `hono.js boilerplate`。
 
你现在是资深的 Node.js 工程师。我有一个初步的想法,需要你通过向我提问,帮助我澄清需求、挖掘边缘场景,最终目标是理清我做一个通用后端功能的脚手架需要实现哪些功能。并按顺序输出一个实现这些功能的大纲。
 
请开始你的提问。

效果: Claude 会像资深 PM 一样向你提问,你逐一回答这些问题后,AI 会生成一份完整的功能清单文档。


步骤 3:测试先行 + AI 执行代码

这是整个工作流的核心环节,必须让 AI 严格按照 TDD 测试方案执行代码。

执行策略

  1. 任务拆解 → 将需求拆分为最小可测试单元
  2. 红阶段 → 先编写测试用例(此时测试应当失败)
  3. 绿阶段 → 编写最小化实现代码,使测试通过
  4. 重构阶段 → 在测试通过的基础上,优化代码结构和性能

关键约束

在全局规则中强制要求 AI:

  • ✅ 任何业务代码提交前,必须有对应的单元测试覆盖
  • ✅ 测试用例需包含正常场景 + 边界场景 + 异常场景
  • ✅ 测试执行必须通过,覆盖率不低于 80%
  • ✅ 严禁为了赶进度而跳过测试环节

步骤 4:代码审核 + 规则反馈循环

AI 执行代码后,进入审核阶段,分为 AI 自动审核和人工审核两部分。

AI 自动审核

AI 每次完成任务时,需要自动进行:

  • Eslint 校验
  • TypeScript 类型校验

如果报错,AI 自己修复直到通过(可以限制修复次数,避免死循环)

人工审核

前期必须进行人工审核,一旦发现问题,思考如何抽象成规则或 Skill,避免 AI 再次犯错。

核心发现: 你会发现后期 AI 执行的效果会越来越好。对于 CRUD 场景,基本不需要人审核了,大概看一下产出代码就知道没问题。


步骤 5:持续迭代与精准化

随着迭代轮次增加,形成正向循环:

迭代轮数 ↑
    ↓
规则精确度 ↑  → AI 执行准确率 ↑
    ↓
处理边界场景的能力 ↑
    ↓
人工介入频率 ↓
    ↓
开发效率 ↑

这就是 AI 时代的竞争力所在: 不是盲目信任 AI,而是通过不断的反馈循环来「教」AI,逐步固化和优化工作规范。


我举个自己实践中的迭代案例:

  • 例如我让 ai 实现超时中间件的时候,ai 是自己原生实现的,然后我感觉这种很常用的功能一般都有现成的库,就查了一下,果然是有 'hono/timeout' 这个库,然后就在全局规则加入了类似:”优先使用社区成熟,稳定的库解决问题“。
  • 例如我在设计后端的 url 的时候,突然想起 K8s 有一个类似的 url 设计规范,可以跟权限结合起来,例如,/api/v1/roles/{roleId},其中 roles 代表就是资源,roleId 代表的是子资源。
resources: ["roles"] # 操作的资源
verbs: ["get"] # 操作类型,增删改查
resourceNames: ["{roleId}"] # 可选(精确到某个子资源)

简单就是说一个 url 代表对什么资源的什么操作。

HTTP 请求 RBAC
GET /roles/{id} verbs: ["get"]
GET /roles verbs: ["list"]
POST /roles verbs: ["create"]
DELETE /roles/{id} verbs: ["delete"]

然后就可以结合我们的 rbac 模型(权限模型),用来标识一个权限是什么,简单来说就是资源名 + 操作,就能标识一个权限。

然后我干脆让 ai 把这个 url 规范抽象为一个 skill,下次 ai 定义 url 的时候就会调用这个规则。

  • 还有很多例子就不一一列举了...

需要注意的是,字节内部的复杂度是更高的,我们后期也会探索,例如字节内部还有:

  • 多 Agent 系统,比如主 Agent 来负责 plan 的制定,有 Coder Agent 负责编码,还有测试 Agent, test Agent 等等,这种多 Agent 协作,我个人估计字节迟早会推出一个开源库让我们使用的,所以不必着急。
  • 评测体系,会对 AI 输出质量进行打分,也就是评测 AI 的输出质量,这条我们目前这一版是靠人工来识别,但我觉得还好,前期人工介入,后期规则越来越好,模型能力越来越强,就可以进入 AI 自我评测阶段了。
  • 可观测性体系:就是对于 AI 写错的地方,是否能知道哪一步错了,然后根据这个让 AI 自动修正 Prompt,然后自动修正全局规则或者抽象为 SKILL。

上面要这么玩,目前个人还是很难的,需要大的平台支持,本文主要是先走通一个小循环,也欢迎大家一起交流(如果觉得不错,感谢点赞关注哦,欢迎入群交流~)。

项目实战:通用后端脚手架

技术栈: Hono.js + Drizzle ORM + PostgreSQL 数据库

前置声明: 整个工作流仅使用免费版工具(Trace + GLM5/豆包模型)即可高质量完成,充分说明该方法论的实际效果令人满意,而非纸上谈兵。

源码获取: 因外链限流问题,需要的朋友可以联系我获取完整代码(github)

脚手架目前的架构图如下:

image.png

第二部分:企业级技术要点

分享上述实战过程中,网上 Node.js 教程很少提及的企业级最佳实践。

优雅关闭(Graceful Shutdown)

无论部署在 Kubernetes、Docker Compose 还是物理机,都需要实现优雅关闭逻辑。

为什么重要?

当应用报错或升级时,容器编排系统会执行关闭流程:

应用故障/升级 → 容器启动关闭流程
  ↓
向 PID 1 进程发送 SIGTERM 信号
  ↓
开启倒计时(默认 10 秒)
  ↓
如果 10 秒后进程未退出,发送 SIGKILL(强制杀死)

问题场景

例:电商扣款业务
 
1. 扣除用户余额 ✅
2. Docker 信号来了,进程被强杀 🔥
3. 积分增加 ❌(未执行)
 
结果:用户钱扣了但没到账,投诉炸裂。

问题根源: Docker 强杀是瞬间的,Node.js 无法执行完事件循环中的剩余回调。

解决方案

优雅关闭可以让你在收到信号后,停止接收新请求,但把内存中已排队的写操作执行完。同时及时释放系统资源(如数据库连接),避免占满最大连接数。


链路追踪(TraceId)

TraceId 是请求在系统中的唯一标识符,从进入系统到返回响应,始终伴随整个生命周期。

为什么需要?

场景:前端用户报错
用户说:"我提交表单后,收到错误 ID:abc123def456"
 
后端排查:
❌ 日志有 1000 条,怎么找到那条错误?
✅ 按 traceId = abc123def456 过滤,立即定位问题

Node.js 的特殊性

与 Java/Go 等多线程模型相比,Node.js 的单进程模型在处理 TraceId 时有本质区别:

语言 模型 上下文隔离方案 难度
Java/Go 多线程/协程 ThreadLocal ⭐ 简单
Node.js 单进程 AsyncLocalStorage ⭐⭐⭐ 复杂

错误做法

方案 1:全局变量

let traceId;  // 全局变量
 
app.use((req, res, next) => {
  traceId = generateId();  // 请求 A 的 traceId
  next();
});
 
// 问题:请求 B 来了,traceId 被覆盖,日志全乱

方案 2:函数传参

// controller → service → dao,每层都要传 traceId
// 代码极其丑陋,难以维护
 
async function getUserOrder(traceId, userId) {
  const user = await getUser(traceId, userId);
  const order = await getOrder(traceId, user.id);
  return { user, order };
}

正确方案:AsyncLocalStorage

Node.js 官方在 async_hooks 基础上,封装了更高级、性能更优的 API:

import { AsyncLocalStorage } from 'async_hooks';
 
const traceIdStorage = new AsyncLocalStorage();
 
// 在请求中间件中创建隔离上下文
app.use((req, res, next) => {
  const traceId = generateId();
  
  // 在当前上下文中存储 traceId(自动隔离)
  traceIdStorage.run(traceId, () => {
    next();
  });
});
 
// 任何地方都可以取,不用传参
function getTraceId() {
  return traceIdStorage.getStore();
}
 
// 使用示例
async function getUserOrder(userId) {
  const traceId = getTraceId();  // 直接取,无需传参
  logger.info(`[${traceId}] Fetching user`, { userId });
  
  const user = await getUser(userId);
  logger.info(`[${traceId}] User fetched`, { userId: user.id });
  
  return user;
}

日志集成

const logger = createLogger((level, msg, meta) => {
  const traceId = getTraceId();
  const logEntry = {
    timestamp: new Date().toISOString(),
    level,
    traceId,    // 自动注入
    message: msg,
    ...meta,
  };
  console.log(JSON.stringify(logEntry));
});

TDD 测试驱动开发

TDD 是企业级后端项目的核心质量保障手段,在 AI 协作开发模式下更是确保代码质量的关键。

核心流程:红-绿-重构

  1. 红阶段 → 编写测试用例,预期会失败(功能未实现)
  2. 绿阶段 → 实现最小化代码,使测试通过
  3. 重构阶段 → 优化代码结构,保持测试通过

Hono.js 项目中的实践

采用 Hono 原生集成测试方案,结合 Vitest 测试框架:

// test/user.test.ts
import { describe, it, expect } from 'vitest';
import app from '../src/app';
 
describe('User API', () => {
  it('should return 404 for non-existent user', async () => {
    const res = await app.request('/api/users/9999', {
      method: 'GET'
    });
    
    expect(res.status).toBe(404);
    const data = await res.json();
    expect(data.code).toBe(0);
    expect(data.message).toBe('User not found');
  });
  
  it('should create a new user', async () => {
    const res = await app.request('/api/users', {
      method: 'POST',
      body: JSON.stringify({
        name: '测试用户',
        email: 'test@example.com',
        password: 'password123'
      }),
      headers: {
        'Content-Type': 'application/json'
      }
    });
    
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.code).toBe(1);
    expect(data.data.name).toBe('测试用户');
  });
});

表格驱动测试

对于多分支逻辑和边界情况,采用表格驱动测试风格:

describe('User Validation', () => {
  const testCases = [
    {
      desc: '缺少必填字段',
      body: { name: '测试用户' },
      expectedStatus: 400,
      expectedMessage: 'Email is required'
    },
    {
      desc: '邮箱格式错误',
      body: { name: '测试用户', email: 'invalid-email' },
      expectedStatus: 400,
      expectedMessage: 'Invalid email format'
    },
    {
      desc: '密码长度不足',
      body: { name: '测试用户', email: 'test@example.com', password: '123' },
      expectedStatus: 400,
      expectedMessage: 'Password must be at least 6 characters'
    }
  ];
  
  test.each(testCases)('$desc', async ({ body, expectedStatus, expectedMessage }) => {
    const res = await app.request('/api/users', {
      method: 'POST',
      body: JSON.stringify(body),
      headers: { 'Content-Type': 'application/json' }
    });
    
    expect(res.status).toBe(expectedStatus);
    const data = await res.json();
    expect(data.message).toBe(expectedMessage);
  });
});

请求超时处理

请求超时处理是后端服务稳定性的重要保障,可以防止长时间运行的请求占用系统资源。

为什么需要?

  • 保护用户体验:与其让用户等待 30 秒,不如在 5 秒内返回"请求超时"
  • 防止系统雪崩:大量超时请求堆积会导致 CPU/内存被迅速耗尽

API 接口级超时

利用 Hono 自带的 timeout 中间件:

import { timeout } from 'hono/timeout'
 
// 1. 全局配置:所有请求默认 5 秒超时
app.use('/api/*', timeout(5000))
 
// 2. 局部配置:针对耗时操作,允许更长时间
app.get('/api/export', timeout(30000), async (c) => {
  // 执行耗时操作...
  return c.json({ success: true })
})
 
// 3. 自定义超时后的逻辑
const customTimeout = timeout(5000, {
  onTimeout: (c) => {
    return c.json({ code: 0, message: '服务器繁忙,请稍后再试' }, 408)
  }
})

数据库级超时

API 层超时只是"切断了回传给用户的路",但数据库内部的任务可能仍在运行。需要更细粒度的控制:

// Drizzle ORM 配置:通过底层驱动设置超时
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
 
const queryClient = postgres(process.env.DATABASE_URL, {
  timeout: 5,          // 建立连接超时 (秒)
  idle_timeout: 20,    // 空闲连接释放
  max_lifetime: 60 * 30 // 连接存活最大时间
})
 
// 在业务代码中手动控制单次查询超时
async function getSlowData() {
  return await db.select().from(users).execute();
}

全局错误处理

在复杂的后端系统中,错误可能来自业务逻辑、数据库约束、第三方 API 失败或语法错误。没有统一处理的话,返回给前端的可能是难看的堆栈信息。

设计原则

  1. 收口原则 → 业务代码通过 throw 抛出错误,由顶层中间件统一拦截处理
  2. 分类分级 → 区分"预期内错误"和"预期外错误"
  3. 安全性 → 生产环境下严禁将详细 Stack 返回给客户端

实现方案

步骤 1:定义标准错误类

// src/utils/errors.ts
export class AppError extends Error {
  constructor(
    public statusCode: number,
    public message: string,
    public code: number = 0 // 自定义业务状态码
  ) {
    super(message);
    this.name = 'AppError';
  }
}

步骤 2:配置全局捕获钩子

import { Hono } from 'hono';
import { AppError } from './utils/errors';
 
const app = new Hono();
 
app.onError((err, c) => {
  const traceId = c.get('traceId') || 'unknown';
  
  // 1. 处理已知业务异常
  if (err instanceof AppError) {
    return c.json({
      code: err.code,
      message: err.message,
      traceId
    }, err.statusCode as any);
  }
 
  // 2. 处理参数校验错误
  if (err.name === 'ZodError') {
    return c.json({
      code: 400,
      message: '参数验证失败',
      details: err,
      traceId
    }, 400);
  }
 
  // 3. 处理未知错误
  console.error(`[Fatal Error] [${traceId}]:`, err);
 
  return c.json({
    code: 500,
    message: process.env.NODE_ENV === 'production' 
      ? '服务器内部错误' 
      : err.message,
    traceId
  }, 500);
});

步骤 3:业务层使用

export async function deleteUser(id: string) {
  const user = await db.findUser(id);
  
  if (!user) {
    throw new AppError(404, '用户不存在', 10001);
  }
  
  return db.delete(id);
}

RBAC 权限控制

RBAC(基于角色的访问控制)是中后台系统最通用的权限模型。通过"用户-角色-权限"的关联,实现权限的解耦。

为什么不直接判断角色?

如果代码里写 if (user.role === 'admin'),当新增一个"超级编辑"角色也需要此权限时,得修改所有代码。判断权限点(Permission)而非角色名,才是系统扩展性的关键。

核心概念

  • 用户 (User) → 拥有一个或多个角色
  • 角色 (Role) → 如 Admin、Editor、Viewer
  • 权限 (Permission) → 如 user:create、order:delete

实现方案

步骤 1:定义数据模型

// 简化版 schema
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  role: text('role').default('viewer'),
});
 
// 权限映射表
const ROLE_PERMISSIONS = {
  admin: ['user:all', 'post:all'],
  editor: ['post:edit', 'post:create'],
  viewer: ['post:read'],
} as const;

步骤 2:实现 RBAC 中间件

// middleware/rbac.ts
import { createMiddleware } from 'hono/factory';
import { AppError } from '../utils/errors';
 
export const checkPermission = (requiredPermission: string) => {
  return createMiddleware(async (c, next) => {
    const user = c.get('user');
    
    if (!user) {
      throw new AppError(401, '未授权访问');
    }
 
    const userPermissions = ROLE_PERMISSIONS[user.role] || [];
    
    // 支持通配符或精确匹配
    const hasPermission = userPermissions.some(p => 
      p === requiredPermission || p === `${requiredPermission.split(':')[0]}:all`
    );
 
    if (!hasPermission) {
      throw new AppError(403, '权限不足,无法执行此操作');
    }
 
    await next();
  });
};

步骤 3:在路由层应用

const api = new Hono();
 
// 只有拥有 post:create 权限的角色才能访问
api.post('/posts', checkPermission('post:create'), async (c) => {
  return c.json({ message: '发布成功' });
});
 
// 管理员专属接口
api.get('/admin/stats', checkPermission('user:all'), async (c) => {
  return c.json({ stats: '...' });
});

日志轮转

在生产环境中,如果所有日志都无限制地写入同一个文件,最终会导致磁盘爆满和日志文件难以打开。

核心目的

  1. 防止单个文件过大(难以检索、占用磁盘空间)
  2. 自动化归档(按日期分类)
  3. 过期清理(例如只保留最近 14 天的日志)

实现方案:Winston + Daily Rotate File

import winston from 'winston';
import 'winston-daily-rotate-file';
 
const transport = new winston.transports.DailyRotateFile({
  filename: 'logs/application-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  zippedArchive: true,           // 历史日志压缩
  maxSize: '20m',                // 单个文件超过 20MB 也会切分
  maxFiles: '14d',               // 只保留最近 14 天的日志
  level: 'info',
});
 
const logger = winston.createLogger({
  transports: [
    transport,
    new winston.transports.Console()
  ]
});

应对 DDoS 攻击

DDoS 攻击的本质是发大量垃圾请求,导致带宽占满、CPU/内存耗尽、连接数耗尽。

现实: 普通企业很难防住大规模 DDoS,目的是提高攻击成本。

限流

在接入层(Nginx)—— 粗筛

性能极高,在流量进入 Node.js 之前就拦截:

limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req zone=api burst=20;

在应用层(Middleware)—— 精滤

灵活度高,根据业务维度限流:

// 限制某个登录用户每分钟只能发 5 条评论
app.use(rateLimit({
  windowMs: 60 * 1000,
  max: 5,
  keyGenerator: (c) => c.get('user').id
}));

限制请求体大小

防止内存溢出 (OOM):

// 攻击场景:发送 2GB 垃圾字符的 JSON POST 请求
// 后果:Node.js 进程尝试分配 2GB 内存,很快就 Out of Memory
 
// 解决:在 Nginx 层配置
client_max_body_size 1m;

Helmet 安全头

Helmet 通过设置各种 HTTP 响应头,自动防御常见的 Web 漏洞(XSS、点击劫持、MIME 类型嗅探等)。

性价比最高的安全加固方案。

Hono.js 官方支持 hono/helmet 中间件,在入口文件 src/app.ts 中引入即可:

import { helmet } from 'hono/helmet';
 
app.use(helmet());

告警机制

告警机制是"及时发现问题"的关键,通过监控关键指标,在异常情况下主动通知相关人员。

告警规则设计

根据应用的 SLA,定义不同严重等级:

export const alertRules = [
  {
    name: 'High Error Rate',
    condition: 'error_rate > 5%',
    severity: 'critical',
    duration: '5m',
    action: 'page_oncall',  // 立即电话/Slack 通知
  },
  {
    name: 'High Response Latency',
    condition: 'p95_latency > 1000ms',
    severity: 'warning',
    duration: '10m',
    action: 'send_to_slack',
  },
  {
    name: 'Database Connection Pool Exhausted',
    condition: 'db_connections > 90%',
    severity: 'critical',
    duration: '1m',
    action: 'page_oncall',
  }
];

与监控系统集成

使用 Prometheus + Alertmanager:

# prometheus.yml
global:
  scrape_interval: 15s
 
scrape_configs:
  - job_name: 'hono-app'
    static_configs:
      - targets: ['localhost:3000']
    metrics_path: '/metrics'
 
alerting:
  alertmanagers:
    - static_configs:
        - targets: ['localhost:9093']

多渠道通知

export async function sendAlert(
  title: string,
  message: string,
  severity: 'critical' | 'warning' | 'info'
) {
  const timestamp = new Date().toISOString();
 
  // 1. Slack 通知
  if (severity === 'critical' || severity === 'warning') {
    await axios.post(process.env.SLACK_WEBHOOK_URL, {
      text: `[${severity.toUpperCase()}] ${title}`,
      attachments: [{
        color: severity === 'critical' ? 'danger' : 'warning',
        text: message,
        ts: Math.floor(new Date().getTime() / 1000),
      }],
    });
  }
 
  // 2. 邮件通知(仅限 critical)
  if (severity === 'critical') {
    await sendEmail({
      to: process.env.ALERT_EMAIL,
      subject: `🚨 CRITICAL: ${title}`,
      html: `<h2>${title}</h2><p>${message}</p><p>${timestamp}</p>`,
    });
  }
 
  // 3. 记录到数据库
  await db.insert(alerts).values({
    title,
    message,
    severity,
    createdAt: new Date(),
  });
}

性能测试

性能测试是确保应用在生产环境中稳定运行的最后一道防线。

基准测试(Benchmarking)

使用 Autocannon 进行简单的吞吐量和延迟测试:

# 安装 Autocannon
npm install -g autocannon
 
# 基准测试:100 并发,持续 30 秒
autocannon -c 100 -d 30 http://localhost:3000/api/users
 
# 输出示例
# Req/Sec: 1234
# Latency: { mean: 45.2, p50: 42, p95: 78, p99: 120 }

压力测试(Load Testing)

使用 K6 模拟真实用户行为:

// load-test.js
import http from 'k6/http';
import { check, sleep, group } from 'k6';
 
export const options = {
  stages: [
    { duration: '2m', target: 100 },
    { duration: '5m', target: 100 },
    { duration: '2m', target: 200 },
    { duration: '5m', target: 200 },
    { duration: '2m', target: 0 },
  ],
};
 
export default function () {
  group('User API', () => {
    // 测试获取用户列表
    let listRes = http.get('http://localhost:3000/api/users');
    check(listRes, {
      'list status is 200': (r) => r.status === 200,
      'list response time < 100ms': (r) => r.timings.duration < 100,
    });
 
    // 测试创建用户
    let createRes = http.post('http://localhost:3000/api/users', {
      name: `user-${__VU}-${__ITER}`,
      email: `user-${__VU}-${__ITER}@example.com`,
      password: 'password123',
    });
    check(createRes, {
      'create status is 200': (r) => r.status === 200,
    });
 
    sleep(1);
  });
}

运行压力测试:

# 安装 K6
npm install -g k6
 
# 执行测试
k6 run load-test.js

数据库性能测试

// src/tests/db-performance.test.ts
import { describe, it, expect } from 'vitest';
import { db } from '../db';
 
describe('Database Performance', () => {
  it('should query 10k users in < 500ms', async () => {
    const start = performance.now();
    const users = await db.query.users.findMany({ limit: 10000 });
    const duration = performance.now() - start;
 
    expect(users.length).toBe(10000);
    expect(duration).toBeLessThan(500);
  });
 
  it('should create 1k users in batch < 2s', async () => {
    const data = Array.from({ length: 1000 }, (_, i) => ({
      name: `user-${i}`,
      email: `user-${i}@example.com`,
      password: 'hashed-password',
    }));
 
    const start = performance.now();
    await db.insert(users).values(data);
    const duration = performance.now() - start;
 
    expect(duration).toBeLessThan(2000);
  });
});

数据持久化与备份

数据持久化本质上解决的是:当系统崩溃、误操作、甚至被攻击时,数据还能不能恢复?

重要认知: 数据库 ≠ 数据安全。数据库只是"存储",而备份 + 恢复能力才是安全的核心。

备份脚本示例

#!/bin/bash
set -o pipefail  # 核心:捕获管道中任何一步的错误
 
DB_NAME="your_db"
BACKUP_FILE="/data/backups/db_$(date +%Y%m%d).sql.gz"
 
# 执行备份
pg_dump -U admin -d $DB_NAME | gzip -1 > $BACKUP_FILE
 
# 检查备份是否成功
if [ $? -ne 0 ]; then
    echo "❌ 备份失败!清理空文件..."
    rm -f $BACKUP_FILE
    # 调用告警机制
    # sendAlert "Database Backup Failed" "pg_dump connection error" "critical"
    exit 1
else
    echo "✅ 备份成功"
fi

可观测性(Observability)

可观测性与监控的区别:

  • 监控 → 告诉你"系统出了问题"(基于预定义的指标和阈值)
  • 可观测性 → 告诉你"系统为什么出了问题"(通过日志、指标、链路追踪)

可观测性的三大支柱

支柱 1:结构化日志

// src/utils/logger.ts
import winston from 'winston';
 
const logger = winston.createLogger({
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.errors({ stack: true }),
    // 自定义格式化,确保输出为结构化 JSON
    winston.format.printf(({ timestamp, level, message, ...meta }) => {
      return JSON.stringify({
        timestamp,
        level,
        traceId,
        message,
        ...meta,
      });
    })
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' }),
  ],
});

支柱 2:指标收集(Metrics)

使用 Prometheus 收集性能指标:

// src/utils/metrics.ts
import promClient from 'prom-client';
 
// 创建指标
export const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request latency',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.1, 0.5, 1, 2, 5],
});
 
export const dbQueryDuration = new promClient.Histogram({
  name: 'db_query_duration_seconds',
  help: 'Database query latency',
  labelNames: ['operation', 'table'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1],
});
 
// 暴露 Prometheus 指标端点
export function registerMetricsRoute(app: Hono) {
  app.get('/metrics', (c) => {
    return c.text(promClient.register.metrics());
  });
}

支柱 3:链路追踪(Traces)

已在前面的 TraceId 部分详细说明。


最后

这套工作流的核心理念就是 持续反馈、不断优化。感谢您的阅读,建议点赞收藏,我们下期再见!

⚡精通 Claude 第 1 课:掌握 Slash Commands

Slash Commands 是 Claude Code 中的快捷方式,通过 / 触发。55+ 内置命令、Skills、自定义命令、MCP 提示词都通过这种机制工作。本文覆盖常用命令、自定义 Skills 创建、以及实战技巧。


slash-command.png

什么是 Slash Commands

Slash Commands 是 Claude Code 中的核心交互机制。在对话中输入 / 开头的指令,Claude 会直接执行对应操作,而不是继续对话。

/help        → 显示帮助
/clear       → 清空对话
/plan        → 进入计划模式
/compact     → 压缩上下文

image.png 这不是对话的延续,而是命令执行。这是 Claude Code 与普通 AI 对话的本质区别。


内置命令速查

Claude Code 提供了 55+ 内置命令,覆盖日常开发全流程:

高频必备

命令 用途
/help 显示所有可用命令
/clear 清空当前对话(别名:/reset, /new
/plan 进入计划模式,让 Claude 先分析再执行
/compact 压缩上下文,保留关键信息
/diff 查看未提交的文件变更
/model 切换 AI 模型

image.png

Git 工作流

命令 用途
/pr-comments <PR号> 获取 GitHub PR 评论
/branch [name] 创建分支(别名:/fork
/resume [session] 恢复历史对话

image.png

系统状态

命令 用途
/status 版本、模型、账户信息
/cost Token 消耗统计
/stats 每日使用可视化
/context 可视化上下文占用

image.png

image.png

image.png 最近一个月用了27天,真的用了CC就回不去古法编程了😂

image.png

Claude Code 配置

命令 用途
/config 打开设置界面
/hooks 查看钩子配置
/mcp 管理 MCP 服务器
/plugin 管理插件
/theme 切换颜色主题
/permissions 调整工具权限

image.png

核心配置项(已显示)

  • Auto-compact:自动压缩冗余对话历史,防止上下文溢出
  • Show tips:显示使用提示与快捷键,辅助上手
  • Reduce motion:关闭界面动画,提升响应速度
  • Thinking mode:开启深度推理,保障复杂任务准确性
  • Fast mode:降低推理深度,快速响应(仅 Opus 4.6)
  • Rewind code:创建代码修改检查点,支持一键回退
  • Verbose output:输出详细调试日志,用于排错
  • Terminal progress bar:显示任务进度,直观查看状态
  • Show turn duration:标注单次交互的耗时,评估性能
  • Default permission mode:控制文件 / 命令操作的默认权限(手动确认 / 允许 / 拒绝)

隐藏配置项(12 more below)

  • Show line numbers:生成代码时自动显示行号
  • Show timestamps:对话历史中显示时间戳
  • Model selection:设置默认使用的模型(Sonnet/Opus/Haiku)
  • Context window limit:手动设置上下文窗口的最大 Token 数
  • Sandbox mode:开启沙箱隔离,限制危险操作
  • Custom hooks:配置自定义脚本(执行前 / 后触发)
  • Keyboard shortcuts:自定义操作快捷键,提升效率
  • Output format:设置响应内容的输出格式(文本 / Markdown/JSON)
  • Allowed tools:指定 Claude 可使用的工具列表
  • Disallowed tools:禁用 Claude 的指定工具
  • Prompt caching:开启提示词缓存,加速重复请求
  • Max thinking tokens:限制思考模式可使用的最大 Token 数
  • Status line customization:自定义终端状态栏的显示内容

image.png/hooks 就是给 Claude Code 加自定义规则、自动脚本、触发动作的地方。

你可以把它理解成:给 AI 助手装插件、设规矩、让它自动帮你干活。

举几个最容易懂的例子:

  1. 每次 Claude 改代码前,自动备份文件
  2. 代码保存后,自动跑 lint 检查格式
  3. 禁止 Claude 访问某些敏感文件
  4. 每次生成代码后,自动格式化
  5. 让 Claude 每次启动都加载你的项目规则

本质是什么?

hooks = 你给 AI 定的自动化小规则。不用你手动点、不用你重复输命令,AI 会按你的规矩自动运行。

image.png

image.png

image.png

image.png

Bundled Skills:内置技能包

Skills 是增强版的 Commands,可以打包脚本、模板和参考文件:

Skill 用途
/batch <instruction> 使用 worktree 并行执行大规模修改
/claude-api 加载项目语言的 Claude API 参考
/debug [description] 开启调试日志
/loop [interval] <prompt> 定时重复执行提示词
/simplify [focus] 检查代码质量

/batch 是 Claude Code 的批量任务执行工具,让你一次性给 AI 发一长串任务清单,它会按顺序自动跑完所有任务,不用你一次次手动发指令、等回复。 你可以把 /batch 理解成:给 Claude Code 开了个「自动流水线」

平时用 Claude Code 是「一问一答」:你发一个需求,AI 做完一个,你再发下一个,全程要手动跟进。而 /batch 就是把你所有要做的事,一次性打包成一个「任务清单」喂给 AI,它会自动按顺序、不中断地把所有任务全部执行完,中间完全不用你手动干预。

比如你要给一个前端项目做这些事:

  1. 把所有组件的 console.log 都删掉
  2. 给所有接口请求加统一的错误捕获
  3. 给所有按钮加 loading 状态防重复点击
  4. 跑一遍 eslint 修复格式问题
  5. 生成一份修改说明文档

如果不用 /batch:你要分 5 次发指令,每次等 AI 做完,再手动发下一个,全程要盯着。用了 /batch:你把这 5 个任务一次性写进 /batch,然后去喝杯咖啡,回来 Claude 已经把所有任务全做完了,直接给你最终结果。


/debug 是 Claude Code 的问题排查工具,专门用来显示后台详细日志,帮你快速找到 AI 为什么出错、卡壳、不干活。

image.png 平时用 Claude Code,你只能看到 AI 给你的最终结果:代码、回答、提示。但 AI 内部到底干了什么、调用了什么工具、读了哪些文件、哪里卡住了,你是看不见的。

/debug 就是把这些 “后台秘密” 全部亮出来给你看。

开启后,Claude Code 会显示:

  • AI 正在调用什么命令
  • 读取 / 修改了哪些文件
  • 为什么拒绝执行某个操作
  • 哪里报错、哪里超时、哪里卡住
  • 模型思考过程、工具执行结果

/loop 是 Claude Code 的自动循环重试工具,让 AI 自己反复检查、修改、运行代码,直到满足要求为止,不用你反复手动提醒。

平时你让 AI 写代码、改 Bug,经常会出现这种情况:AI 改一次 → 运行报错 → 你告诉它错了 → 它再改一次 → 又报错 → 你又得提醒……

来回折腾特别麻烦,效率很低。

/loop 就是解决这个问题的:你开启循环模式,AI 会自己 “闭环干活”

它会自动做这几件事:

  1. 改代码
  2. 运行测试 / 检查报错
  3. 自己发现哪里错了
  4. 自动再次修改
  5. 直到运行成功、没有错误,才停下来告诉你完成

简单说:你只需要说需求,剩下的反复调试让 AI 自己循环搞定,不用你插手。


/simplify 命令可以用来优化代码

image.png


自定义 Skills:打造你的命令体系

Skills 是 .claude/skills/<name>/SKILL.md 文件:

image.png

mkdir -p .claude/skills/optimize

文件:.claude/skills/optimize/SKILL.md

---
name: optimize
description: 分析代码性能问题、内存泄漏和优化机会
argument-hint: <代码片段>
allowed-tools: Bash, Read, Grep
---

# 代码优化分析

分析以下代码的性能问题:

$ARGUMENTS

检查:
1. 是否有不必要的重复计算
2. 是否存在内存泄漏风险
3. 算法复杂度是否有优化空间
4. 是否有冗余的 DOM 操作或 API 调用

变量替换

全部参数:

---
name: fix-issue
description: 修复 GitHub Issue
---

修复 Issue #$ARGUMENTS

单个参数:

---
name: review-pr
description: 评审 PR
---

评审 PR #$0,优先级 $1

动态上下文(Shell 命令):

---
name: commit
description: 创建带上下文的 Git 提交
allowed-tools: Bash(git *)
---

## 当前状态

- Git 状态:!`git status`
- 文件变更:!`git diff HEAD`
- 当前分支:!`git branch --show-current`

## 任务

基于以上变更,创建提交信息。

文件引用

请审查以下实现:
@src/utils/helpers.js
对比 @src/old-version.js 和 @src/new-version.js

Frontmatter 完整参考

---
name: my-command           # 命令名称(成为 /my-command)
description: 用途描述      # 帮助 Claude 判断何时使用
argument-hint: <参数>      # 自动补全提示
allowed-tools: Bash, Read  # 无需审批即可使用的工具
model: opus                # 指定使用的模型
disable-model-invocation: true  # 仅用户可调用
user-invocable: false      # 从 / 菜单隐藏
context: fork              # 在隔离子代理中运行
agent: general-purpose     # 子代理类型
hooks:                     # 技能级钩子
  PreToolUse: []
  PostToolUse: []
  Stop: []
---

命令执行流程

sequenceDiagram
    participant User
    participant Claude
    participant FS
    participant Shell

    User->>Claude: /optimize
    Claude->>FS: 搜索 .claude/skills/ 和 .claude/commands/
    FS-->>Claude: 返回 optimize/SKILL.md
    Claude->>Claude: 解析 Frontmatter
    Claude->>Shell: 执行 !`command` 替换
    Shell-->>Claude: 返回命令输出
    Claude->>Claude: 替换 $ARGUMENTS
    Claude->>User: 处理提示词
    Claude->>User: 返回结果

实战技巧

1. 用 /plan 避免浪费

在执行大改动前,输入:

/plan 重构用户认证模块

Claude 会先分析代码、制定计划,你确认后再执行。避免做到一半发现方向错了。

2. 用 /compact 保持专注

长对话变慢时,/compact 压缩上下文,保留核心信息,速度恢复。

3. 自定义快捷命令

把常用的复杂操作封装为 Skill:

---
name: deploy
description: 部署到生产环境(仅用户可调用)
disable-model-invocation: true
allowed-tools: Bash(npm *), Bash(git *)
---

1. 运行测试:npm test
2. 构建:npm run build
3. 推送到部署目标
4. 验证部署状态

4. MCP 提示词作为命令

MCP 服务器暴露的提示词可以直接调用:

/mcp__github__list_prs
/mcp__github__pr_review 456
/mcp__jira__create_issue "Bug 标题" high

Skill vs Legacy Command

特性 Skills(推荐) Legacy Commands
位置 .claude/skills/<name>/SKILL.md .claude/commands/<name>.md
目录结构 支持打包文件 单文件
自动触发 支持 不支持
子代理执行 context: fork 不支持
优先级 更高 较低

同名时,Skill 优先。


常见问题

命令不生效?

  • 确认文件在 .claude/skills/<name>/SKILL.md.claude/commands/<name>.md
  • 检查 frontmatter 的 name 字段
  • 重启 Claude Code 会话

Skill 和 Command 冲突?

  • 删除其中一个,或重命名
  • 同名时 Skill 总是优先

总结

Slash Commands 是 Claude Code 的核心交互范式:

  • 内置命令:55+ 覆盖开发全流程
  • Bundled Skills:开箱即用的增强能力
  • 自定义 Skills:打造个人命令体系
  • MCP 提示词:扩展到外部工具

下一课我们将深入 Memory 系统,学习如何让 Claude 记住项目上下文。


延伸阅读

OpenClaw 跟病毒的区别是什么?

节日期间在家办公,我坐在书房的电脑前,盯着满屏飘红的终端😖

webpack_error_terminal_style_match.png

我没有中勒索病毒,也没有被黑客攻击。我只是在之前,极其手欠地给跑在后台的 OpenClaw 下达了一句简单的语音指令:帮我把这个老项目里的无用 npm 依赖清理一下,顺便跑通本地编译。

openclaw_feishu_chat_conversation.png

然后我就去客厅看电视了。

等我两个小时后回来,发现风扇狂转。打开终端一看,这玩意儿不仅把我的 package-lock.json 给删了,还因为有个老旧的 Sass 模块死活装不上,它自己去网上搜了个不知道谁写的 Python 脚本跑了一遍,顺手把我的全局 Node 环境降级到了两年前的版本,最后还在根目录下给我留了几十个不知名的临时编译文件🤬🤬。

看着这片惨状,我脑子里突然冒出一个极其荒诞的问题: 一个拥有系统最高执行权限的 OpenClaw,跟一个木马病毒的区别到底是什么?

如果仔细推敲,你会发现这两者的行为轨迹惊人地相似,甚至可以说,前者带来的工程灾难往往更具欺骗性。


在搞破坏?

以前我们在电脑上跑个脚本,报错了就停在那,等你来排查,过程相对可控的。

但现在的 OpenClaw 是个拥有极高自主性的 Agent。它最大的卖点是遇到问题会自动尝试解决。这在写写单纯的文本时是个优点,但在复杂的现代前端工程里,这就是个彻头彻尾的灾难🤔。

当一个病毒遇到权限阻断时,它会疯狂尝试提权、扫描端口、注入进程。 那 OpenClaw 遇到前端编译报错时会干嘛?

它会像一个极其鲁莽的瞎子:

  • 它发现 pnpm install 报错了,它不会去思考是不是内网镜像源挂了,而是自作主张把它换成 npm,瞬间摧毁你精心维护的 Monorepo 幽灵依赖机制(symlink)。
  • 它发现有个类型找不到,它不会去查 .d.ts 声明,而是极其粗暴地去改你 node_modules 里的源码,或者给你全剧加上 @ts-ignore
  • 如果遇到文件死锁,它甚至敢在终端里直接替你敲下 rm -rf

病毒搞破坏是为了勒索你,而 OpenClaw 把你的系统搞崩溃,仅仅是因为它想完成你那句帮我跑通编译。

后台静默执行

做了 9 年研发,我看过无数次因为一行配置写错导致的线上 P0 级事故。所以越是资深的工程师,越在乎执行边界。

我们为什么需要 Git?为什么需要 Code Review?为什么 CI/CD 要分发不同的环境权限? 因为我们要清楚地知道,谁在什么时候,动了哪行代码,引发了什么后果。

但 OpenClaw 打破了这个铁律。它是一个跑在你电脑或者服务器上的巨大黑盒。

当你让它接管你的工作流时,你根本不知道它为了完成目标,在后台下载了多少个包含潜在风险的三方库。你也不知道它在调用工具链的时候,有没有把你的环境变量、甚至包含数据库密码的 .env 文件当成上下文,明文发送给了大模型的 API 服务器。

传统病毒窃取你的隐私,至少还会偷偷摸摸加个壳。而 OpenClaw 是你亲手把服务器的 Root 权限双手奉上🙌。


一段真实的案例

为了让大家更直观地感受这种病毒式操作。我复盘了一下昨天它为了修复一个构建错误,所做出的决策链路。

这是一个典型的高级前端遇到问题时的处理逻辑:

1. 发现 Webpack 构建报 chunk error 错误

screenshot-20260407-151125.png

2. 检查报错日志,发现是内存溢出 (OOM)

screenshot-20260407-151038.png

3.修改启动脚本,增加 Node 内存上限,完事🤷‍♂️

export NODE_OPTIONS="--max-old-space-size=4096"
npm run build

而 OpenClaw 在面对同一个报错时,它的终端执行记录:

# 1. 发现构建报错
npm run build 

# 2. 它看不懂 OOM,以为是依赖问题,执行了极其致命的操作
rm -rf node_modules
rm package-lock.json
npm install --legacy-peer-deps

# 3. 依赖彻底乱套,报了更多编译错误。它决定更换打包工具
npm install -g vite
# 强行在原本的 Webpack 项目里塞入一段残缺的 vite.config.js

# 4. 依然跑不通,它甚至去改了操作系统的环境😖😖😖
sudo chmod -R 777 ./ 

看到最后那个 chmod 777 的时候,我后背都在冒冷汗😢。 它为了解决一个权限微小的编译告警,直接把你整个项目的安全底裤都给扒了。这不是病毒是什么?


我想泼一盆冷水🫡

现在全网都在狂欢,各大社区都在比拼谁的 Agent 更聪明,谁能让电脑完全自己写代码、自己部署。

很多人沉迷于这种看着终端自己跳动代码的爽感中,觉得这就是未来。

但我不得不泼一盆冷水。在工程领域,不可控的自动化,比纯手工还要危险一万倍😒。

不管是 OpenClaw 还是未来更牛的智能体,只要它不具备真实世界的工程常识和后果承担能力(到目前为止都不可能为你背锅!!!),它就是一个披着 AI 外衣的高危病毒。

咱们在敲下回车之前,脑子里想的是:这会影响线上吗?会引发竞态问题吗?接手的同事能看懂吗? 而 Agent 脑子里只有计算概率:根据统计学,下达这个指令,满足用户当前 prompt 的概率哪个最大?它不在乎你的硬盘会不会被占满,不在乎你的生产环境会不会被污染。


所以,咱们这些在一线干活的兄弟们,清醒一点。

工具终究是工具,它可以帮你查 API,可以帮你写正则,可以帮你生成模版代码。但千万别把系统的控制权和架构的决策权,交给一个随时可能暴雷的 AI Agent

把危险关在沙盒里,让执行处于监控下。如果你做不到这一点,那你电脑里跑着的那个每天对你嘘寒问暖的 OpenClaw,真的比熊猫烧香还要可怕的。🤔

对此大家怎么看?

Suggestion.gif

目前中国大陆唯一可以免费在 Xcode 中使用顶级大模型智能编程的方法

在这里插入图片描述

0.引子

现今,在中国大陆想要使用最强编程大模型在 Xcode 中实时交互的方法不多。

为了体验 Vibe Coding 的“畅快”打击感(或许还有等待间隙时的些许失落感),我们往往需要在 Cursor 和 Xcode 间无限切换,这多少有点让秃头小码农们有些不爽快!

在这里插入图片描述

况且第三方智能编程 IDE 与 Xcode 联合开发还有一个问题:就是从 Xcode 外部无法精确的感知和处理 Xcode 中的细枝末节。举个例子:宝子们见过 Cursor 为了修复 1 个 bug 却新产出 10 个 bug 的蛋疼壮观场面吗?

在这里插入图片描述

幸运的是,在 Xcode 最新正式版 26.4 中: 在这里插入图片描述

我们找到一种免费且非常简单就可以辅以超强编程大模型(gpt-5.4 或 gpt-5.3-codex 家族)的方法:

在这里插入图片描述

操作起来也非常简单,目前(2026.4.7号)并不需要付费 OpenAI 账号或绑定任何国际银行卡。

在这里插入图片描述

这样宝子们“足不出户”就可以在 Xcode 里享受氛围编程的乐趣了哦。

在这里插入图片描述

废话少叙,心动不如行动!

让我们马上开始操练起来,将 Xcode 打造为丝毫不输于 Cursor 的智能 IDE 吧!8-)


1.工欲善其事,必先利其器

首先,大家需要下载和安装 Xcode 26.4 正式版。

同时,必须保证我们可以访问到 ChatGPT 官网,否则还扯什么呢?

在这里插入图片描述

2.启用 Xcode 智能 Agent

运行 Xcode ,打开设置,进入 Intelligence 页面:

在这里插入图片描述

Xcode 26.4 支持先进最强的 2 个编程大模型智能体(Agents):ChatGPT Codex 和 Claude,不过目前后者在大陆无法登录,会提示:当前区域的服务不可用。

在这里插入图片描述

所以,我们只有“稍微”退而求其次一丢丢,来使用 gpt-codex 了。

点击 Codex 右侧的 Get 按钮,下载并安装 Agent 到本地,我们能看到只有 77MB,可谓相当“小鸟依人”:

在这里插入图片描述

接下来的一步就是进入 Codex 智能体(Agent)页面,登录 ChatGPT 账户即可:

在这里插入图片描述

如图所示,在登录了 gpt 账号之后,我们可以就可以恣意选择自己喜爱的 gpt 大模型啦:

在这里插入图片描述

不过据我观察,Xcode 智能 Agent 中的 gpt 编程大模型貌似有点缩水,少了不少强力模型哦(比如 GPT-5.3 Codex High 和 GPT-5.3 Codex Extra High 等):

在这里插入图片描述

但话又说回来,对于这免费的“飞来横福”,我们还要什么自行车呢?


注意:正如之前所说的,目前只需免费的 ChatGPT 账号即可,且不需要绑定任何银行卡。

但是,未来还能不能享用这“免费的午餐”,就有点世事难料了。


在这里插入图片描述

3. 测试

在上面各步骤都就绪之后,我们就可以找一个项目实际在 Xcode 中小试身手了。

下面,打开宝子们最爱的项目,先让 Xcode Agent 为我们总结一番吧:

在这里插入图片描述

当然,在 Xcode 里编程智能体做的不仅仅是做个总结那么“弱智”,我们还可以让它直接分析 Xcode 中拥有的一切:

在这里插入图片描述

现在,直接在 Xcode 中用 AI 来修正编译错误不再是梦想了:

在这里插入图片描述在这里插入图片描述

这样做可以最大化利用 Xcode 丰富的上下文来让 AI 充分考虑和修正问题,避免了外部智能 IDE(比如 Cursor、Qoder 等)无必要的切换和折腾。


想用 Xcode 与本地大模型“双剑合璧”来协同编程的宝子们,请移步如下链接观赏精彩的内容:


看到这,不知宝子们心动了吗?

在这里插入图片描述

要不要一起来借助 Coding Intelligence 来试试 Xcode 的氛围编程呢?8-)

若有任何与本文相关的配置问题,请宝子们毫不犹豫的私我哦!

感谢观赏,下次再会吧!

在这里插入图片描述

苹果的罕见妥协:当高危漏洞遇上“拒升”潮 -- 肘子的 Swift 周报 #130

issue130.webp

苹果的罕见妥协:当高危漏洞遇上“拒升”潮

对于 iOS 用户来说,最近或多或少都会看到与 Coruna、DarkSword 有关的高危漏洞消息。两个攻击链均采用水坑攻击的方式,攻击者无需受害者进行任何交互,仅需访问一个被植入恶意 iframe 的合法网站或加载恶意广告,即可触发完整的攻击链,在窃取资料后自动清理攻击痕迹。由于工具链利用的漏洞存在于 iOS 13 至 18.7 的绝大多数版本中,截至目前,已有上亿用户受到影响。

Coruna 主要针对 iOS 13 至 iOS 17 的设备,在过去几个月间,苹果已为这些系统推送了多次安全更新。DarkSword 则主要针对 iOS 18.4 至 18.7 的设备。尽管这部分设备均具备升级至 iOS 26 的硬件条件,但由于种种原因,仍有不少 iOS 18 用户选择按兵不动。

在很长一段时间里,苹果用户对于系统更新的态度都相当积极,这也是苹果生态的一大特色。但这一趋势在去年出现了变化——Liquid Glass 带来的巨大视觉冲击,让苹果用户中第一次出现了相当比例主动拒绝升级到 iOS 26 的现象。与此同时,为遵守英国《网络安全法》(Online Safety Act)的要求,苹果在 iOS 26.4 中为英国用户引入了强制年龄验证机制,由于验证条件严苛,不少成年用户甚至被系统强行锁入‘儿童模式’,进一步推动了英国用户停留在 iOS 18 或 iOS 26.3 的风潮。而拒绝安装新版本,意味着这部分用户同时放弃了后续所有安全补丁,让设备进一步暴露在潜在风险之下。

面对这一局面,苹果承受了明显的舆论压力与品牌风险。特别是在 3 月下旬,DarkSword 的完整攻击代码被泄露到了 GitHub 上,让这一国家级黑客工具瞬间平民化,直接迫使苹果必须采取紧急行动。最终,苹果罕见地为 iOS 18 单独推出了安全补丁 iOS 18.7.7,将原本仅用于 iOS 26 的防护机制回移植到旧系统。至此,苹果完成了针对本次高危漏洞的全部官方安全响应。

无论是苹果还是生态中的开发者,大多希望用户能积极跟进系统更新——既能减少多版本适配的维护负担,也能让用户尽快享受到新 API 带来的便利。但现实是,始终有一部分用户出于性能、续航、使用习惯乃至隐私等方面的考量,有意将设备锁定在某个版本。

本次事件或许会带来两个方向上的变化:苹果在压力下调整了长期坚守的更新策略,为刻意留守旧系统的用户做出了妥协;而事件本身的广泛传播,也可能促使更多用户从安全角度重新审视“能不更新就不更新”的惯性,回到积极更新的轨道。这种双向的改变,或许正是这场风波意料之外的收获。

本期内容 | 前一期内容 | 全部周报列表

近期推荐

通过 Animatable 深入 SwiftUI 动画 (Animatable in SwiftUI Explained - Complete Guide with Examples & Deep Dive)

网络上并不缺少探讨 SwiftUI 动画机制的文章,但 Sagar Unagar的这篇仍然提供了一个颇具启发性的切入点。他没有从隐式或显式动画入手,而是围绕 Animatable 协议做了一次系统梳理:从 animatableData 的作用,到 AnimatablePair 如何承载多个插值参数,再到通过自定义 VectorArithmetic 让更复杂的数据结构参与动画。文章最值得注意的一点在于其核心视角:SwiftUI 实际上是在“动画数据”,而非直接对视图进行动画处理。


在 Swift Package 中共享本地化资源 (Localization in Swift Packages)

Xcode 能为 .xcstrings 文件自动生成类型安全的 Swift 符号,但这些符号仅在资源所在的 module 内可见——一旦将本地化资源抽离为独立的 Localization 包,其他 feature 包便无法享受编译期检查的优势。Khan Winter 的解决方案相当直接:通过一个 bash 脚本解析 .xcstrings 的 JSON 结构,生成 public extension LocalizedStringResource 扩展,使所有模块都能以 .l10n.helloWorld 的形式访问翻译键。

其中一个颇具参考价值的细节是 Debug 模式下的 @dynamicMemberLookup 设计——访问不存在的键时仅记录日志而不崩溃,而在 Release 构建中仍保留完整的编译期校验。相比基于 Swift 可执行文件的方案,这种实现更加轻量,复制脚本即可使用。


Coordinator 全局导航模式 (SwiftUI Coordinator Pattern: Navigation Without NavigationLink)

尽管 SwiftUI 一直在丰富基于状态驱动的导航 API,但管理全局导航一直是 SwiftUI 中的一个“痛点”。Wesley Matlock 以一个五 Tab 的音乐收藏应用为例,展示了如何通过 Coordinator 模式将导航决策从 View 中抽离:用一个 Route 枚举统一描述所有目的地,由单一的 Coordinator 对象持有导航状态并执行跳转,View 只需声明“去哪”而无需关心“怎么去”。文章没有回避 NavigationPath 不透明、路由携带模型对象导致的 Hashable 困境等实际问题。对于大多数中等规模的 SwiftUI 应用来说,这是一个务实且易于落地的导航治理方案。


把 Hacking with Swift 的编程风格写进 AI (Teach your AI to write Swift the Hacking with Swift way)

Paul Hudson 和他的 Hacking with Swift 让很多开发者走上了 Swift 与 SwiftUI 的学习之路。在 AI 时代,Paul 不仅推出了面向苹果开发生态的各类专业 Skill,也开始尝试在与 AI 的协作中注入更具个人特质的编程风格。

在本文中,他分享了一份极具辨识度(且充满他标志性幽默)的 AGENTS.md 配置。这套规则不仅约束了 AI 的技术选型,还为 AI 注入了 Paul 的灵魂:强调先展示结果再解释原理、偏好清晰而非炫技、甚至包括在代码写得漂亮时适时地喊出一句 "Boom!"。与其说这是一份用于 AI 的“系统提示词”,不如说是在为 AI 定义一种编码哲学——某种程度上,这种方式正在将冷冰冰的“代码生成”推向带有人情味的“风格迁移”。


AI Agent 的道与术

在刚过去的 Let's Vision 2026 中,王巍(Onevcat) 发表了关于在大型开发团队中应用 AI Agent 的演讲。整场分享讨论的重点,并不是某个具体工具有多强,而是当代码实现成本被迅速压低后,团队该如何重新组织开发流程,以及工程师的价值该如何重新定位。

作为 LINE 应用开发团队的一员,Onevcat 在过去几个月中的工作重心也已明显发生变化。用他自己的话说,他正在逐步从传统意义上的 iOS 工程师,转向探索如何将 AI 应用于服务大型产品研发团队的实践者。这种角色上的变化,也让这场分享比一般的工具介绍更有说服力。

演讲围绕三个关键问题展开:如何控制上下文污染,如何把个人经验沉淀为团队可复用的 memory 与 skill,以及如何让协作模式从“人指挥多个 Agent”逐步走向更自动化的闭环。里面有不少相当接地气的实践建议,例如将 AGENTS.md 控制在精简范围内、为 Agent 提供模块定位与架构速查脚本、鼓励 Claude Code、Codex、OpenCode 等多种 harness 并存,以及通过 webhook、cron、pipeline 和自动验收机制让 Agent 真正进入团队流程。

演讲稿仓库 中不仅包含完整的 Slidev 源码,也保留了不少演讲配套材料,包括原始资料收集和与 AI 协作的完整 trace,值得一并阅读。


从零开始:用 AI 开发一个 iOS 原生 APP 完整指南

我经常会在社交媒体上看到一些零基础的“开发者”通过 AI 构建了自己的产品或服务。尽管我使用 AI 的时间也不短,但我仍然比较困惑:这条路径真的像大家描述的那样有效吗?Zachary Zhang 分享了他完全借助 AI 工具,从零构建并上架一款纯原生 iOS 应用(SwiftUI + Cloudflare 后端)的实战全过程。这篇文章最让我印象深刻的,是他严谨的“工程化管线”:在让 AI 写代码前,必须先生成结构化的 PRD 和 HTML 格式的视觉参考;而在工具选择上,他在项目“从 0 到 1”的冷启动阶段,极力推荐 Claude Code 等终端工具,以便更好地统览全局,一次性构建出合理的多文件项目架构。

或许你和我一样,对于 100% 基于 AI 的开发方式仍存疑惑。但在代码生成越来越廉价的今天,开发者的核心壁垒,正在加速向“需求精准拆解”、“系统架构把控”以及“面向报错的全局调度能力”转移。

工具

Slots:提高自定义 SwiftUI 组件设计效率的宏

将多个视图组合封装成可复用组件,是 SwiftUI 开发中的常见需求,对团队内部开发者或第三方库作者来说更是如此。但当组件包 title、icon、image、action 等多个泛型 View 插槽后,初始化器的组合数量往往会迅速膨胀。Kyle Bashour 创建的 Slots 宏,正是为了解决这类多 slot 组件的样板代码问题。

开发者只需声明组件的 slot 属性,宏便会按组合自动生成所需的初始化器,无需手写大量 init 重载。对于需要支持文本便捷写法的 slot,还可以通过 @Slot(.text) 自动获得 LocalizedStringKeyString 版本的初始化方式。 Slots 很适合用于构建设计系统中的 Card、Row、Banner、Toolbar 这类既要支持简单调用、又要保留高度定制能力的组件。


Explore SwiftUI:纯原生组件与修饰符的视觉速查图库

尽管 Apple 官方文档的质量在逐年改善,但对于以声明式和视觉驱动为主的 SwiftUI 来说,官方文档中依然缺乏足够直观的代码与 UI 效果对照,尤其是同一组件在 iOS、macOS 和 visionOS 等多平台上的表现差异。很多时候,开发者为了实现某个特定的 UI 细节,往往会去求助于复杂的第三方库或手写冗长的自定义视图,却忽略了 SwiftUI 本身可能已经提供了绝佳的原生解决方案。Florian 建立的 Explore SwiftUI 站点,正是一个为了解决这一痛点而生的“视觉速查字典”。它摒弃了任何第三方封装,纯粹以展示 Apple 官方内置组件的原生能力为核心。所有的代码示例都被剥离了无关的业务逻辑,保持极简,配以高质量的视觉预览,开发者只需“复制、粘贴、运行”即可直接验证效果。

书籍

SwiftUI Architecture: Patterns and Practices for Building Scalable Applications

这是一本 Mohammad Azam 在不久前出版的新书。它不是一本教你如何使用 VStack 或编写动画的入门书,而是一本纯粹探讨 SwiftUI 应用架构、数据流和现代工程化实践的进阶读物。

书中提供了大量直击生产环境痛点的解决方案,例如:如何构建全局的 Sheets 和 Toasts、如何利用 NavigationPath 设计解耦的多 Tab 编程式路由、以及如何使用 Property Wrapper 编写优雅的表单验证。尤为重要的是,作者并不是要向你灌输某种死板的架构模式,而是旨在帮助你建立真正的声明式心智模型。

或许有人觉得,在 AI 辅助编程盛行的时代,这类探讨架构的书籍还重要吗?借用 Mohammad Azam 在书中的观点:AI 让代码生成变得廉价,但也正因如此,系统架构的设计(边界的划分和状态所有权的明确)变得比以往任何时候都更加重要。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

如何在本地跑 Core ML 模型识别呼噜声,并用 iCloud 优雅同步?

大家好,我最近开发了一款App《SleepDiary(睡眠声音日记)》。

9771791b1b272012179e60c5853cedc8.jpg

作为一款睡眠监测类 App,核心业务逻辑可以用一句话概括:

录一整夜的音,把打呼噜和说梦话的片段摘出来,最后生成睡眠报告。

看似简单,但在工程实现上却困难重重:

  1. 隐私与成本问题:长达 8 小时的音频绝对不能一整段传到服务器端,这不仅会直接把你的服务器带宽跑破产,还会被用户骂死(谁敢把在卧室一整夜的录音全传到网上?)。
  2. 性能与功耗问题:放在端侧跑模型,势必要使用长时间的后台保活,如何避免手机发热和 OOM (Out Of Memory)?

经过最近这段时间的研究,我用 AVFoundation + Core ML + SwiftData 的纯血原生技术栈把这套流程跑通了。今天就和大家分享一下我的实现思路与踩坑日记。

一、端侧的 AI:硬核从零训练自己的鼾声分类模型

最初的设计方案很简单粗暴:开个录音,每秒去判断分贝,超过阈值就保存。但这完全不行,深夜翻身的声音、空调声、外面的汽车声都会被误判。

市面上现成的声音分类模型要么太大(动辄上百MB),要么对“鼾声”、“梦话”这种特定场景不够敏锐。于是我决定硬核一点——自己动手,从收集数据开始训练一个专用的轻量级神经网络(SnoreWave.mlpackage)。

1.1 数据收集与模型训练

为了让模型足够精准,我花了大量时间收集开源数据集并结合自己实录的各种“打雷级”打呼声(最终 1.2w 条数据)。 把杂乱的音频转换成模型能“看懂”的输入是第一步——将音频流转化为梅尔频谱图(Mel-spectrogram) 。这相当于将一维的声音信号,变成了二维的图像图像特征,然后再喂给我用深度学习框架搭建的 CNN(卷积神经网络)进行分类。

模型训练收敛后,我依靠 coremltools 将其转换为了 Apple 原生支持的 .mlpackage。为了控制 App 包体积并保证低功耗运转,这个模型被我极致压缩,剥离了非必要分支,达到了极高的预测效率。

1.2 AVAudioEngine 实时截流送显

有了自己的模型,下一步就是在 iOS 端跑通流式推理。 我们不使用高层的 AVAudioRecorder,而是使用 AVAudioEngine。因为它允许我们通过 installTap 在音频流经过的过程中“截胡”到 AVAudioPCMBuffer

然后在端侧把这个 Buffer 原样转化成模型需要的数组输入:

// 截胡音频流的伪代码
let inputNode = audioEngine.inputNode
let format = inputNode.inputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 4096, format: format) { [weak self] (buffer, time) in
    // 捕获到音频帧后,交给我们自定义的分类器管线
    self?.audioCaptureService.processAudioBuffer(buffer)
}
audioEngine.prepare()
try audioEngine.start()

1.3 降维打击 OOM 崩溃:用 Actor 隔离模型生命周期

坑点来了!如果每次截取到一个 buffer,都在主线程或者随机的 Dispatch Queue 去实例化这个自定义模型进行预测,一晚上下来你的 App 必因为内存暴涨被系统强制 Kill 掉(Jetsam Event)。

解法:引入 Swift Actor 隔离与复用机制 

在《睡眠声音日记》中,我是用全局唯一的 Actor 来维持模型的单一生命周期,使用环形缓冲区去缓存几秒钟的声音片断,组合后一次性输出:

swift
actor EventDetectionPipeline {
    // 全局唯一持有我们自己训练好的模型实例
    private let model = try? SnoreWaveformCNN(configuration: MLModelConfiguration())
    
    func processAudioWindow(_ window: AudioWindow) async {
        // 将音频转化成梅尔频谱所需的 MLMultiArray
        guard let multiArray = window.toMLMultiArray() else { return }
        
        // 发起端侧离线推理
        if let prediction = try? model?.prediction(input: multiArray) {
            if prediction.classLabel == "snore" {
                // 命中目标:触发存储!
                await persistCapturedEvent(label: .snore)
            }
        }
    }
}

通过自己训练轻量级模型 + Actor 的串行数据处理,保证了模型资源的极致释放。即便后台连续疯狂推理 8 个小时,CPU 的平均占用率也能被压在极低的水平,用户即使整晚充着电,手机也完全不发烫。

二、存储的艺术:音频文件与 SwiftData 模型分离

识别完事件后,怎么持久化? 这引发了第二个大问题——千万别把音频这种大块二进制流全都写进 SwiftData 或者 Core Data!

2.1 相对路径是王道

我的存储策略是:结构化数据走 SwiftData(打点时间、标签量化数据),音频文件走沙盒原生写入。 在《睡眠声音日记》的 SleepEventRecord 模型中,我只存了一个相对路径(filePath)。

@Model
final class SleepEventRecord {
    var timestamp: Date
    var duration: TimeInterval
    var eventLabel: EventLabel // .snore, .speech, .cough
    var filePath: String? // 只存相对路径: "20240315/snore_0234.m4a"
    
    init(timestamp: Date, eventLabel: EventLabel) {
        self.timestamp = timestamp
        self.eventLabel = eventLabel
    }
}

为什么要相对路径?  因为沙盒路径(UUID)在每次应用重签或重新安装时是会变的。如果存绝对路径,第二天文件全找不到了!读取时,永远使用 FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(filePath) 动态拼装。

2.2 防治 iCloud 把服务器挤爆

录了一晚上的高音质 M4A 文件,如果不加限制,系统的 iCloud 备份会自动把它们全传上去。用户那可怜的 5GB iCloud 很快就会爆满。因此,我在写入音频文件后,立马用原生 API 给文件打上“拒绝备份”的 Tag:

var url = documentDirectoryURL.appendingPathComponent(fileName)
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true // 保护用户的 iCloud 空间!
try url.setResourceValues(resourceValues)

三、私有 CloudKit 的优雅同步体验

音频不用同步了,但我们的 SleepSessionRecord(当晚评分,鼾声次数统计,分析数据)需要跨设备(尤其是和 Apple Watch 联动时)和防止在用户删掉 App 重装后丢失。

以往做 Core Data + CloudKit 繁琐得让人想死。但在 iOS 17 的 SwiftData 下,它变成了真正的“优雅”:我们甚至可以动态控制它的开启闭合。

我把开关值存到了 NSUbiquitousKeyValueStore(KVS),根据这个从远程同步过来的用户偏好,动态初始化 ModelContainer

// 在 SleepDiaryApp.swift 入口处动态配置容器
var sharedModelContainer: ModelContainer = {
    let schema = Schema([SleepSessionRecord.self, SleepEventRecord.self])
    
    // 读取 UserDefaults/KVS 的 iCloud 开关
    let isCloudSyncEnabled = UserDefaults.standard.bool(forKey: "iCloudSyncEnabled")
    
    let configuration = ModelConfiguration(
        schema: schema,
        isStoredInMemoryOnly: false,
        // 如果开启,赋予 private 数据库标识;如果不开启,设为 .automatic 或本地优先
        cloudKitDatabase: isCloudSyncEnabled ? .private("iCloud.xxxxx") : .none
    )
    
    do {
        return try ModelContainer(for: schema, configurations: [configuration])
    } catch {
        fatalError("Could not create ModelContainer: (error)")
    }
}()

依靠云端大容量配额下的 .private 标识,只要用户打开 iCloud 同步,他们在换了新手机后重新下载 App,所有的睡眠数据历史记录就会像魔法一样哗哗哗回到列表中。


四、写在最后

开发《睡眠声音日记》的这段时间里,我最大的感触是:苹果的原生护城河真的很香。  只依靠一套 Swift 兵器库:从 SwiftUI 的丝滑动画绘制、到 HealthKit 获取深度睡眠的联动、再到 Core ML 的底层加速,这是以往杂糅其它中间件完全得不到的性能优势和开发爽感。

感兴趣的同行们,可以在 App Store 搜  “睡眠声音日记-SleepDiary”  下载把玩一下,有任何架构或技术点上的建议,大家评论区见,或者私下找我交流。也欢迎吐槽!

一墙之隔,不同的时空 -- 肘子的 Swift 周报 #129

issue129.webp

一墙之隔,不同的时空

一年一度的 Let's Vision 大会在上海如期举行,今年的主题是:“Born to Create, Powered by AI”。除了与 Swift、空间计算相关的常规 Session,大会还邀请了许多开发者分享他们在工作中对 AI 的应用与理解。通过这些讲师对 AI 工作流的介绍,我也受益匪浅。原本只能容纳 300 人的 AI 主题会场,里三层外三层站满了热情高涨的观众。

然而,在众多优秀的 Session 中,一场由 YuChe Cheng 准备的、名为《Let's Create 1-liner Code in Swift》的演讲却将我的注意力引向了另一个会场。这究竟是一个怎样的话题?带着疑问我走了进去。作为一个 LeetCode 积分 2200+ 的开发者,YuChe Cheng 在演讲中展示了如何通过 Foundation 以及 Swift Algorithms 提供的大量高阶函数,将原本平淡无奇的 For-loop 代码,转换成更加优雅、美观、极具 Swift 风格的 Function Chaining(1-liner code),并在易读性与性能之间取得了很好的平衡。

看着幻灯片上的 Function Chaining 被一次又一次地优雅迭代,我有种茅塞顿开的畅快。整整 30 分钟的演讲,让我始终处于一种纯粹的兴奋之中——这种感觉,通常只在我绞尽脑汁最终攻克了一个难题,或是深刻理解了一个新概念后才会涌现。

尽管与主会场只有一墙之隔,但由于 AI 话题的绝对热度,本场演讲的听众明显偏少。与其说我为许多人错失了一场精彩演讲而感到遗憾,我真正担心的其实是:随着 AI 的进一步渗透,许多开发者原本在追求功能之外所赋予代码的那份“气质”,会不会就此消亡?

开发者不应该只关心编译后冷冰冰的二进制功能,代码本身也是个人风格的载体。它就像文章一样,在输出逻辑与结果之外,还承载着美学表达,体现着编写者的个人品味与巧思。

在今年的 Let's Vision 上,我感觉我们正站在一个时间的十字路口:我们是该一味追求 AI 带来的极致高效,还是在拥抱变化的同时,依然让属于开发者的那份骄傲与手艺,在 AI 时代得以保留?

本期内容 | 前一期内容 | 全部周报列表

本期推荐

Swift 6.3 Released

从 Swift 6 开始,语言演进已经稳定在半年一个 minor 版本的节奏,上周 Swift 6.3 如期发布。与前几个版本相比,这一版本并未引入明显的重磅特性,更多是对既有体系的打磨:并发模型在诊断准确性方面有所改进,新增的 @c 特性(attribute)进一步强化了 C/C++ 互操作能力,同时编译优化的控制粒度也变得更加细致。

尽管如此,这一版本也释放出一个清晰的信号:Swift 正在从“以 Apple 平台为中心的应用开发语言”,逐步向“具备跨平台与系统级能力的通用语言”演进。Embedded Swift、Android 支持的持续推进,以及 SwiftPM 构建体系的统一,都在指向这一方向。对多数 iOS 开发者而言,短期体感或许有限,但从更长的时间维度来看,这更像是一次为未来铺路的基础性更新。


如何在 Swift 中承接尚未稳定的 JSON (Designing a type-driven JSON in Swift)

当 API 契约尚未稳定、前后端对字段的理解又经常漂移时,Swift 的强类型系统反而会放大数据与 JSON 之间转换时的边界问题。Roman Niekipielov 在本文中介绍了一个刻意做小的 JSONValue 类型,用来承接这类过渡阶段的 JSON 数据。相比 [String: Any],它保留了更明确的类型结构;相比直接编写 Codable 模型,又更适合应对频繁变化的契约。这个实现并不试图替代正式模型,而是将不确定性暂时限制在边界层。


Swift 原生 AI Agent 开发实践系列

市面上有大量开发者使用 Python、TypeScript 开发 AI Agent,但 Chris Karani 认为,Swift 的并发模型天然更适合 Agent 的隔离与调度,强类型系统和宏功能也带来了额外的安全保证。他用 6 篇文章、从多个角度实践了这一观点——从统一多个 LLM Provider 的 SDK Conduit,到基于 Apple Foundation Models 的 Agent 运行时 Colony,再到用 Metal 加速的上下文记忆管理。如果你正在考虑在 Apple 平台上构建 AI 功能,这个系列是目前少见的完整原生方案。


Liquid Glass 设计工作坊 (Talking Liquid Glass with Apple)

Danny Bolella 在纽约参加了苹果举办的 Liquid Glass 设计工作坊,与设计团队和 SwiftUI 工程师进行了为期三天的深入交流。本次活动传递出非常明确的信号:Liquid Glass 并非过渡性尝试,而是苹果未来数年的设计方向,且将在后续工具链中成为默认前提。与此同时,苹果反复强调“层级(Hierarchy)”的重要性——界面应围绕内容构建,控件只是服务于内容的辅助元素,应尽量退居边缘,让信息本身成为视觉与交互的中心。除此之外,Danny 还在本文中记录了其他一些 SwiftUI 工程师给出的建议和技巧。本文记录的内容可以帮助你更早理解这场设计演进的节奏与方向。


App Store Connect 大更新 (Apple Dropped 100+ New Metrics. Your Competitors Are Already Using Them)

苹果对 App Store Connect 进行了近年来最大的一次更新,一口气引入了 100+ 官方指标、按来源划分的 cohort 分析、同行基准对比(转化率与单下载收益)以及可通过 API 导出的订阅数据。Jessica Chung 在本文中对这些关键变化进行了系统梳理。由于所有数据均来自苹果一手统计,这意味着开发者在 ASO 和增长决策中,将不再依赖第三方估算,而可以直接基于真实用户行为进行分析与优化。更重要的是,这次更新补齐了长期缺失的关键能力:你可以追踪不同关键词与渠道带来的用户质量,建立从曝光、下载到订阅与续费的完整转化链路,并通过同行基准明确自身所处位置。

本次更新对于开发者而言无疑是利好,但对于部分第三方 App Store 分析服务来说,也在一定程度上提高了竞争门槛,促使其提供更具附加值的能力。


Package Traits in Xcode

在创建 SPM 时,某些依赖可能只被特定 API 使用,但一旦用户引入该包,即便不使用这些 API,也需要一并引入相关依赖。Package Traits 正是为了解决这一问题而引入的,它为 SPM 提供了一种声明可选特性的方式,使使用者能够按需启用功能,从而避免引入不必要的依赖。遗憾的是,在该功能推出后,一直只能在社区版本的 Swift 工具链中使用。随着 Xcode 26.4 的发布,Package Traits 终于获得了苹果官方支持,有望迎来更广泛的应用。Matt Massicotte 在本文中对该特性进行了介绍,并展示了其基本用法。


优化感官性能,让用户感觉更快 (Why your SwiftUI app feels slow even though Instruments says it’s fine?)

用户投诉响应慢,一定是应用性能问题吗?Rafał Dubiel 将关注点从“实际性能”转向“感知性能(Perceived Performance)”,讨论如何通过界面反馈与交互节奏,让用户感觉应用“更快”。例如通过 skeleton view、延迟加载,以及合理的动画与状态过渡来掩盖等待时间。作者指出,在许多场景下,用户体验的关键并不在于减少毫秒级的计算时间,而在于是否及时提供反馈。相比单纯优化性能指标,这种从用户感知出发的思路,往往更直接地影响用户对应用流畅度的判断。


在 SwiftUI 中控制行高 (Adjusting line height in SwiftUI on iOS 26)

iOS 26 为 SwiftUI 新增了 lineHeight(_:) modifier,用于控制文本相邻两行基线之间的距离。Natalia Panferova 在本文中对各种配置方式进行了详细对比:内置预设(.loose.tight)、基于字号倍数的 .multiple(factor:)、固定增量的 .leading(increase:),以及绝对值控制的 .exact(points:)。此外,lineHeight(_:) 与已有的 lineSpacing(_:) 并不相同:前者控制基线间距,后者控制行底到下一行行顶的距离。

Natalia Panferova 曾是 Apple SwiftUI 核心团队成员,参与过多个关键 API 的设计与开发。本月她刚刚出版了新书 The SwiftUI Way,面向有一定 SwiftUI 经验的开发者,聚焦于生产环境中的模式选择、常见反模式识别,以及如何与框架“顺势而为”而非对抗。

工具

Cove:Swift 6 编写的 macOS 开源数据库客户端

Cove 是由 Emanuele Micheletti 开发的一款原生 macOS 数据库客户端,整个项目完全使用 Swift 6 构建,目前已经支持 PostgreSQL、MySQL、MariaDB、SQLite、MongoDB、Redis、ScyllaDB、Cassandra 和 Elasticsearch 等多种后端。它采用 SwiftUI 搭配 AppKit 原生控件实现,没有走 Electron 或 Web 技术栈,因此整体更轻量,也更符合 macOS 用户熟悉的交互体验。

相比“又一个数据库 GUI”,Cove 更值得关注的是它的实现思路。作者将所有数据库能力统一抽象为 DatabaseBackend 协议,UI 层不包含任何针对特定后端的分支逻辑。无论是 SQL 数据库、Redis 这类键值数据库,还是 MongoDB、Elasticsearch 这类非关系型后端,最终都会被整理为统一的表格模型交由界面渲染。项目目前仍处于 v0.1.0 的早期阶段,但已经具备查询、结构浏览、编辑、SSH 隧道和多标签等基础能力。即便你并不打算把它作为日常数据库工具,Cove 依然是一个很值得 Swift 开发者研究的桌面应用架构样本。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

我用 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 真正接进系统、工具和运行环境之后,它开始从“能回答问题”变成“能替你完成任务”。

HelloGitHub 第 120 期

本期共有 40 个项目,包含 C 项目 (1),C# 项目 (4),C++ 项目 (2),Go 项目 (4),Java 项目 (3),JavaScript 项目 (5),Kotlin 项目 (2),Python 项目 (4),Rust 项目 (3),Swift 项目 (2),人工智能 (5),其它 (5)

Claude在得物App数仓的深度集成与效能演进

随着以Claude Code为代表的代码大语言模型(Code Large Language Model,以下简称Code LLM)在软件工程领域的普及,其在企业级数据仓库(以下简称数仓)建设中的应用逐渐从单一的代码补全向全链路辅助演进。本文旨在探讨Code LLM在电商数仓环境下的深度集成逻辑与工程实践。文章首先界定了数据确权中的人机边界,分析了内部数据工具向Agentic工作流演进的趋势,并提出了“认知运行时与执行运行时解耦”的架构范式。

本文认为,大模型在企业级数据仓库中的落地核心,主要体现在两大维度:一是数据确权(Data Rights Confirmation) ,二是规范化输入输出(Standardized I/O) 。以此为框架,结合得物App真实数仓建设与研发实践,系统阐述了基于Galaxy MCP的基础设施集成方案,并对智能视觉埋点、AI OneData建模、智能周报生成、策略孵化中心等典型场景的架构设计与运行逻辑进行深入分析。最后,针对大模型应用中存在的幻觉问题与合规风险,本文提出一套系统性的治理与管控机制。

image.png

一、核心逻辑界定:数仓开发中的人机边界与架构演进

Code LLM引入数仓的建设流程,并非简单的工具替换,而是对现有研发范式、职责边界及工具链架构的系统性升级。在讨论具体的提效场景前,必须首先厘清底层的逻辑支柱;若未能厘清权责边界与架构定位,大模型的引入极易演变为不可控的技术债务与运维风险。

数据确权边界:管理审批与技术实现的分离

数仓建设的工程起点是原始数据(ODS 层)的接入,该环节涉及数据来源的合法性、数据所有权的界定、个人可识别信息(Personally Identifiable Information,PII)的合规审查,以及数据质量的责任归属。这些属性决定了数据接入不仅是技术动作,更是企业内部的核心数据确权过程。在引入 AI 辅助能力时,必须严格区分**「管理审批」「技术实现」**的权责边界:

管理审批(人类主导): 数据的权限审批、合规性定责、业务口径的最终确认,属于具备法律与管理效力的行为。当前法律框架下,AI 不具备独立的民事主体资格,无法独立承担法律与管理责任,因此在确权决策环节,必须由明确的数据治理委员会或业务负责人完成人工审批与权责确认。

技术实现(AI 辅助): 在完成人工确权与审批后,涉及的 DDL 脚本编写、同步任务模板配置、基础数据质量校验(Data Quality Check,DQC)规则生成等技术执行工作,可由 Code LLM 基于已确权的元数据自动化生成,并经人工复核确认后上线执行。

明确这一边界,既保障了企业数据资产的安全与合规,也为后续工程环节的 AI 深度介入提供了合规前提。

内部工具演进:从被动式 SaaS 到 Agentic 工作流

传统的数仓研发平台(如得物 App 内部数据研发平台 Galaxy)、BI 系统及指标字典,在形态上多属于被动式内部 SaaS 工具:即工具仅提供标准化的功能模块与图形化界面(GUI),无法主动理解并完成用户的业务意图,需依赖工程师的专业技能手动操作,属于典型的「人找功能」的被动响应模式,工具的价值上限取决于功能丰富度与用户的专业熟练度。

Code LLM 的引入,促使内部数据工具向 Agentic(智能体化)工作流演进。在这一模式下,核心交互方式由 GUI 转向意图驱动的自然语言交互界面(Language User Interface,LUI);系统不再仅仅提供「编写 SQL 的环境」,而是能够接收业务意图(如「按特定维度统计退款归因」),在预设的权限与规则边界内,通过调用底层 API 自主完成元数据检索、逻辑组装,并输出最终的数据洞察或代码草案。这种演进重构了数据工具的核心价值逻辑:从「为专业人员提供功能组件」,转向「为业务用户交付可落地的任务结果」。

架构范式升级:认知运行时与执行运行时的解耦

在探讨 AI 与现有数仓架构的融合时,需先明确大模型在系统中的核心定位:大模型无法替代 Spark、Flink、ClickHouse 等传统大数据计算与存储引擎的核心算力能力,其核心价值是促成了计算架构中「认知决策」与「执行落地」的解耦分离,我们将其拆解为两个核心模块:认知运行时(Cognitive Runtime)执行运行时(Execution Runtime)

认知运行时: 由 LLM 充当核心载体,负责处理非结构化需求解析、业务逻辑到 SQL 的语义映射、代码规范校验及调优策略生成,核心处理语义与逻辑的推演工作。该模块不直接触碰物理数据,仅在数据权限管控体系的约束下,操作已确权授权的元数据(Metadata)与抽象语法树(Abstract Syntax Tree,AST)。

执行运行时: 由传统大数据计算引擎充当核心载体,负责海量数据的物理扫描、分布式计算与存储落地,核心处理确定性的算力调度与执行任务。

这种解耦架构,使得数仓系统既能保有传统引擎的高吞吐、高可靠特性,又能具备大模型的语义理解与逻辑泛化能力,同时与前文的权责边界、合规要求形成了架构层面的呼应。

本质洞察:规范化的输入与输出(Standardized I/O)

当我们试图用 AI 优化数仓系统时,若仅仅停留在「单点提效」的表层,最终往往会陷入「为了用 AI 而用 AI」的陷阱。大语言模型基于概率分布生成文本,存在固有的幻觉风险;在对数据准确性、口径一致性要求极高的数仓场景中,无约束的自然语言对话式开发(业内俗称 Vibe Coding,即无明确规范、凭感觉自由编码的模式),会导致代码风格发散、业务口径不一致、数据失真等严重问题,甚至引发合规风险。

剥离掉「AI 写代码」的表层形式,触碰数仓与 AI 融合的本质,其核心在于构建规范化的输入与输出(Standardized I/O)契约。无论是埋点设计、OneData 建模,还是周报生成与策略孵化,其底层逻辑高度一致:将模糊的业务需求,通过结构化模板、CSV、JSON 或 API 接口(规范化输入)喂给模型,并强制模型按照预设的 Markdown 模板、DDL 规范或报告框架(规范化输出)进行交付。这种基于规范的驱动开发模式(Spec-Driven Development,SDD),将大模型不可控的自由文本生成,转化为基于规范契约的受限定向编译,从根源上压缩了幻觉的产生空间,构成了 AI 在数仓中规模化应用的安全底座。

综上,明确的数据确权边界,与标准化的输入输出契约,共同构成了 Code LLM 在企业级数仓中安全、合规、规模化落地的两大核心支柱。

二、基础设施底座:Galaxy MCP 的标准化集成

要实现上述的“规范化输入与输出”,大模型必须具备感知和操作企业真实数据环境的能力。在实践中,研发团队基于模型上下文协议(Model Context Protocol, MCP),为内部数据研发平台(Galaxy)构建了标准化的集成底座。

image.png

MCP 协议:大模型与数仓环境的通信契约

Galaxy MCP 充当了 Code LLM与企业内部数据资产之间的桥梁。传统模式下,工程师需要手动复制表结构、日志信息喂给大模型;而在 MCP 架构下,大模型被赋予了“手和眼”。

通过提供统一的 HTTP Streamable 接口与 Bearer Token 鉴权机制,MCP 使得大模型能够在安全受控的前提下,直接调用底层数据平台的 API。这种集成的本质,是为大模型提供了标准化的环境感知输入

核心工具集暴露与场景映射

Galaxy MCP 向大模型暴露了一系列高度结构化的工具(Tools),这些工具构成了 Agent 执行复杂任务的基础原子。核心 API 及其对应的数仓场景映射如下:

  • 分析数据结构: 模型在编写 SQL 前,自动获取目标表的建表语句,确保字段名与数据类型绝对准确,消除幻觉。
  • 追溯数据来源: 在 OneData 建模或排查数据异常时,模型自动查询上游血缘表,梳理复杂的依赖链路。
  • 逻辑审查: 模型直接读取线上调度任务的真实 SQL 逻辑,用于代码重构或口径比对。
  • 排查任务失败: 查找特定时间段内失败的运行实例。
  • 根因分析: 模型获取完整的执行日志(如 Spark 报错堆栈),结合上下文自动分析报错原因并给出修复建议。

IDE 深度集成:鉴权链路

在工程落地中,Galaxy MCP 实现了与主流 AI IDE 的无缝集成。通过上述配置,开发者在 IDE 中只需输入自然语言指令(如:“读这个表试试:xxx.table_name”),底层大模型即可自动路由至 Galaxy MCP,完成鉴权、API 调用与结果解析的闭环。这标志着数仓开发正式迈入 Agentic 时代。

三、工程实践落地:基于规范化 I/O 的效能演进

本章将结合得物App数仓研发实证,以实际应用阐述上述底层逻辑在实际业务线中的落地场景。下面的每一个场景,均是在内部经过多轮POC验证,是“规范化输入与输出”理念的具体投射。

智能视觉埋点:多模态输入到结构化 JSON 的映射

业务背景: 埋点设计是数仓数据采集的前置环节。传统流程长期存在三大痛点:

  • 成本高: PRD 交互复杂,且开发过程中变更频繁,人工对齐耗时。
  • 业务参数发散: 历史迭代频繁、经手人多,同类交互动作命名混乱(如 like_clickclick_like_btn 混用),极大增加下游清洗成本。
  • 质量不可控: 埋点规范弱且参数点状上报,不同业务规范不一致,无法准确判断上线/修改埋点会导致下游哪些核心指标发生异常。

image.png

规范化 I/O 逻辑:

  • 需求解析与确认(前置收敛): 构建规范化的 PRD 理解 Prompt,结合原生多模态模型(如 Gemini 1.5 Pro,保留 UI 设计稿的颜色、层级、空间位置等视觉特征),输出标准化的“埋点提需文档”。经业务多轮确认无误后,再进入实质埋点设计环节。
  • 智能埋点设计(上下文注入): 整合三类核心资产作为模型输入:① 当前页面历史权威埋点字典;② 人工沉淀的埋点规范与经验;③ 离线大模型梳理的“埋点-指标”血缘关系。模型基于此契约输出设计方案。
  • 规范化输出(Schema 强校验): 强制模型输出符合企业 Schema 校验的 JSON 格式,核心实现三点:埋点格式化: event_id 必须严格符合 [event]_[page]_[block] 的格式,且事件与参数定义强绑定业务规范字典,杜绝开发随意造词。参数收敛: 基于历史权威字典,自动映射并收敛同义参数,消除发散。参数完备性: 结合业务场景自动补全必填上下文参数,避免漏埋。

实测效能: 在某社区线双周迭代抽样中,埋点设计人力投入从平均10人日缩减至5人日。更核心的收益在于:

  • 一致性提升至 95%: 通过模型前置校验,有效遏制了存量埋点的无序扩张。
  • 质量提升与规则沉淀: 全面盘点并固化了现有埋点规则,将数据质量卡点前置到设计阶段,降低埋点设计引发数仓下游指标计算的事故率。

AI OneData 建模:血缘 CSV 到标准 DDL 的编译

业务背景: OneData 方法论要求严格的数据分层与指标口径统一。但在人工执行时,面对复杂的表血缘关系,规范的遵守率往往存在波动,且梳理历史口径耗时巨大。一个典型的 OneData 项目,纯人工梳理口径溯源、编写白皮书往往需要耗费数月。

image.png

规范化 I/O 逻辑:

  • 规范化输入: 研发团队摒弃了让模型直接阅读杂乱 SQL 的做法,而是通过脚本预先提取底层表的血缘关系与字段清单,将其转化为高度结构化的 CSV 文件(如 某域onedata_表血缘.csv、某域onedata_字段清单.csv)。这些 CSV 文件连同格式严苛的 Markdown 规范文档(规定了字段分隔符 ##、溯源必须到 ODS 层等)一起作为 Prompt 注入模型。
  • 规范化输出: 模型解析多层嵌套的子查询,严格按照契约输出标准化的口径溯源文档与 Mermaid 架构图(如 引力onedata_表血缘_mermaid.md),以及符合分层规范的 DDL 语句。

实测效能: 在某业务线包含 34 张表、涉及 6 个粒度的 OneData 重构项目中,对比历史同等规模项目的纯人工评估耗时(约 60 人日),采用 AI 辅助与人工复核结合的模式,整体交付周期缩短至 16 人日(提效约 74%)。由于机器执行规范的绝对一致性,文档的格式统一度达到 100%。

智能周报生成:SQL 结果集到业务洞察的转化

业务背景: 传统 BI 报表只能展示数据,无法解释数据。业务方需要的是“为什么跌了”,而不是“跌了多少”。但如果直接让 LLM 读取原始 CSV 数据生成周报,极易出现“幻觉”(如 1+1=3 的计算错误),因为 LLM 本质上是概率模型,不擅长精确的数学运算。

image.png

规范化 I/O 逻辑: 系统设计上存在两条并行路径,而其底层逻辑的起点是同一份 Prompt 规范文档。

该规范文档充当单一可信源(Single Source of Truth):在研发阶段,LLM 读取规范文档中精确定义的字段口径、聚合顺序与格式规则,将其编译为 Python 确定性计算模块(Spec-to-Code);在运行时,同一份规范又作为约束契约传入 LLM,驱动语义叙事输出。这意味着规范的变更(如修改 WoW 计算口径)只需更新一处,两条路径同步收敛。

路径一(Python 计算层): 由 LLM 依据规范编译生成的 Python 模块负责所有确定性运算——WoW/YoY 计算、渠道贡献度排序、量级格式化(万/亿分档)——输出已预渲染的 Markdown 文本片段,不再含任何原始数值。

路径二(LLM 叙事层): 模型接收的是无需再做任何数学运算的结构化文本,其唯一任务是完成跨模块的趋势判断与业务归因叙述(如"供给下降叠加搜索量上升 → 供需错配")。

核心价值: 这一架构的核心价值在于:将 LLM 的不确定性严格限制在语义层,将数值精度的责任锁定在代码层,两者各自处于自身最可控的能力边界内,从根本上规避了"让模型直接计算 CSV 原始数据"所带来的计算幻觉与格式漂移风险。

策略孵化中心:从单点提效到端到端业务策略流

业务背景: 区别于纯粹的 Coding 提效,业务冷启动阶段(如违规作者探查)涉及完整的策略工作流:定义目标 -> 数据收集 -> 特征筛选 -> 模型训练 -> 效果回收。该过程涉及业务方、分析师、数据科学家等多个角色,存在巨大的信息损耗与特征选择的“效率孤岛”。特征选择的质量高度依赖于个人的隐性能力。

image.png

规范化 I/O 逻辑:

策略孵化中心将这一复杂的非线性探索过程,重构为基于 AI Agent 的标准化流水线,包含四大核心模块:

  • 策略工作流模块(输入端): 业务人员输入自然语言描述的业务目标(如“异常作者识别”)。
  • 样本分析与特征泛化模块: Agent 自动调用 MCP 接口检索资产,推荐相关特征(探索已有的未知),并利用 LLM 的常识推理补充行业通用特征(探索未有的未知)。输出标准化的样本拼接表。
  • 模型训练模块: Agent 根据特征类型,自动推荐并调用底层机器学习组件(如逻辑回归 LR、随机森林 RF),标准化输入输出矩阵。
  • 可视化分析模块(输出端): 最终生成包含特征重要性可视化、沙盘推演结果的标准化策略报告。

项目演进里程碑:

  • 第一期(MVP 验证): 完成样本分析模块与模型工厂的基础功能,支持逻辑回归和随机森林,并在“违规作者探查”项目中取得显著的业务增量收益。
  • 第二期(Agent 交互): 开发特征交互式 Agent 的核心对话与 PRD 生成能力,完善特征管理。
  • 第三期(高级分析): 深化可视化模块,完成保序性、显著性等高级统计学分析功能。

实测效能: 策略生成到落地的整体周期由 10 人日缩短至 1-2 人日,提效 3-5 倍。AI 的介入不仅加快了策略迭代的频率(策略新鲜度),更通过标准化流程降低了对个人隐性经验的依赖,使得策略的专业度与准确率显著提升。

智能测试与质量保障:不确定性输出的校验机制

业务背景: 财务级数据指标(如实收、补贴、平台服务费等)具有严格的勾稽关系。针对此类指标编写覆盖全面的边界测试用例耗时极长,且业务语言(如“邮费返利抵减技术服务费”)转化为测试 SQL 极其困难。

image.png

规范化 I/O 逻辑:

  • 规范驱动开发(SDD): 将测试环节前置,定义标准化的测试契约(Schema)。
  • 规范化输入: 将 DDL、业务口径文档及上游数据分布特征作为上下文输入给模型。
  • 智能用例生成: LLM 自动生成覆盖主键唯一性、非空校验、枚举值分布、业务逻辑边界。
  • 闭环诊断: 执行测试 SQL 后,若出现报错或精度异常,LLM 通过 MCP 接口自动读取执行日志进行根因诊断,精准区分“底层逻辑错误”与“浮点数计算带来的可接受精度误差”,并输出修复建议。

实测效能: 构建了“质量守夜人”机制,测试覆盖率大幅提升。在某财务项目中,模型自动生成了 20 余个复杂的校验 SQL,将数据质量卡点前置到开发阶段,显著降低了上线后的数据事故率,实现了从“人工抽测”到“机器全量自动化校验”的范式跃迁。

Spark UI Skill:数仓任务排查与智能调优

业务背景: 数仓日常运维中,Spark 任务的排查与调优(如数据倾斜、OOM、执行计划不合理)高度依赖工程师的个人经验。排查过程需要频繁查看 Spark UI,分析 DAG 图、Stage 耗时、Shuffle 数据量等,耗时且门槛高。

image.png

规范化 I/O 逻辑:

  • 规范化输入: 通过 MCP 接口或监控脚本,自动抓取 Spark UI 的核心指标(如 Stage 耗时、Task 倾斜度、GC 时间、内存使用率)及 SQL 执行计划(Explain 树),将其转化为结构化的 JSON 或文本日志作为 Prompt 注入。
  • 智能诊断推演: LLM 充当“认知运行时”,基于输入的结构化日志,结合历史调优专家经验库(如“Shuffle 阶段数据量剧增且单 Task 耗时极长 → 数据倾斜”),进行逻辑推演。
  • 规范化输出: 强制模型输出标准化的诊断报告,包含:① 根因定位(如 Join 键倾斜);② 具体调优建议(如增加 spark.sql.shuffle.partitions,或改写 SQL 引入 Broadcast Join);③ 优化后的 SQL 代码草案或参数配置。

实测效能: 将单次复杂任务排查时间从数小时缩减至分钟级。不仅大幅提升了运维效率,更将资深专家的调优经验固化为标准化的 Agent 技能(Skill),显著降低了团队的整体技术门槛。

四、提示词工程的系统架构化设计

在上述所有工程实践中,提示词(Prompt)不再是简单的自然语言对话,而是演变为了系统架构的一部分。高质量的提示词工程是实现规范化 I/O 的核心载体。

提示词作为系统配置的演进

在传统的开发模式中,系统配置通常是 YAML、JSON 或 XML 文件,用于指定数据库连接、调度频率等确定性参数。而在 AI Native 的数仓架构中,提示词承载了业务规则、编码规范与逻辑约束,成为了认知运行时的“配置文件”。这些提示词被纳入版本控制系统(如 Git),与底层代码同等对待,接受严格的 Code Review。

结构化提示词的模块化拆解

以智能周报生成场景为例,其核心的 周报数据prompt 采用了高度结构化的模块设计:

# 角色设定 (Role Definition)
你是一位资深的电商数据分析师,擅长从复杂的数据指标中提取业务洞察。
# 核心任务 (Core Task)
请基于提供的 [SQL 结果集 JSON],撰写本周的业务周报。
# 规范约束 (Constraints)
1. 必须使用 Markdown 格式,包含二级标题与无序列表。
2. 严禁捏造数据,所有数值必须来源于输入的数据集。
3. 环比计算公式为:(本期值 - 上期值) / 上期值,保留两位小数。
# 结构模板 (Output Template)
## 一、 核心指标概览
- GMV:[数值] (环比 [百分比])
- 转化率:[数值] (环比 [百分比])
## 二、 异动归因分析
[基于数据波动的具体分析]

这种模块化的提示词设计,将角色设定、任务描述、约束条件与输出模板严格分离,最大程度地降低了模型的幻觉概率,确保了输出结果的工程级可用性。

五、风险管控与治理机制

在电商数仓中引入 Code LLM,必须建立系统性的风险管控体系,以应对大模型固有的技术缺陷及企业合规要求。

幻觉风险的系统性抑制

大模型在处理复杂表关联时,可能捏造不存在的字段或错误理解业务逻辑。管控方案包括:

  • 上下文增强 (RAG) 与 MCP 强绑定: 严禁模型在无上下文的情况下“裸写” SQL。必须通过 Galaxy MCP 实时获取真实的表结构与分区信息,确保模型引用的表名、字段名均真实存在。
  • 强类型校验: 模型生成的 SQL 必须经过数仓平台的语法解析器(Parser)进行静态检查,阻断基础语法错误。

数据安全与合规保障

数据仓库包含大量商业机密与用户隐私,使用 LLM(特别是调用外部公有云 API 时)存在数据泄露风险。管控方案包括:

  • 数据脱敏拦截: 在 Prompt 提交至模型前,必须经过网关层的正则表达式与 NLP 实体识别扫描,自动屏蔽或替换真实的手机号、身份证号及真实交易金额等敏感数据。
  • 元数据隔离: 仅允许模型读取表结构(Schema)与脱敏后的样例数据(Mock Data),严禁模型直接访问生产环境的物理数据。
  • 审计追溯: 所有由 AI 辅助生成的代码变更,在版本控制系统(如 Git)中必须带有特定的 AI 标签,并记录对应的 Prompt 与生成日志,确保事故发生时可进行完整的责任追溯。

六、结论

Code LLM 对电商数据仓库的介入,绝非停留在代码补全的表层提效,而是推动了数仓研发范式的底层演进。通过界定数据确权的管理边界,研发团队能够安全地将技术实现环节交由 AI 辅助;通过引入规范驱动开发(SDD)与 Agentic 工作流,并以“规范化的输入与输出”为核心,有效抑制了大模型的不确定性。

从 Galaxy MCP 的底层基础设施打通,到智能埋点、OneData 建模、周报自动化,再到端到端的策略孵化中心,大模型正在重塑数据流转的每一个节点。在这一演进过程中,数据工程师的核心职责正在发生转移:从繁重的 SQL 编码与基础排错,转向业务逻辑的抽象、规范契约的制定以及系统架构的最终决策。未来,基于 LLM 的认知运行时将与大数据执行运行时更加深度地融合,持续推动数据仓库向智能化、自动化的方向演进。

往期回顾

1.Claude Code + OpenSpec 正在加速 AICoding 落地:从模型博弈到工程化的范式转移|得物技术

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

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

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

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

文 /博温

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

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

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

我的 App 审核被卡了? -- 肘子的 Swift 周报 #128

issue128.webp

我的 App 审核被卡了?

上周四,我 Discord 社区里的一位网友抱怨,说他的应用在 App Store Connect 上提交了四五天,却迟迟没有进入审核状态。就在我还津津有味地跟大伙儿分析原因时,突然心里一紧:我周一提交的应用,好像也一直没收到审核动态?

有网友建议我去申请一下“加急审核”。可当我点进页面时,系统却提示我“没有可加急的应用”。仔细一查才发现,原来是太久没更新 App,业务都生疏了——我的应用虽然完成了所有前置步骤,但我压根儿就没点那个“提交以供审核”的按钮。

补点按钮没过几个小时,应用就顺利上架了。

尽管我这纯属虚惊一场,但最近社区里关于“苹果审核变慢”的讨论确实多了起来。很多人猜测,这或许与近期 Vibe Coding 的盛行有关。虽然没有官方证实,但 Vibe Coding 确实在降低开发门槛的同时,也在短时间内放大了应用提交的数量与迭代频率,从而把压力传导到了审核环节。

事实上,苹果最近也确实对 Replit 这类允许普通用户进行 Vibe Coding 的应用在审核上进行了卡关。即便允许其上架,也要求在核心功能上做出妥协。在 Michael Tsai 关于此事的博客介绍中,我看到了一条非常敏锐的留言:

这些提供 Vibe Coding 功能的应用(本身也是 Vibe Code 的产物),正被用来批量制造纯靠 Vibe Code 生成的 App 并提交上架。

AI 不仅在重塑开发方式,也正在对应用审核与发行体系提出新的挑战。有人或许会问:如果用魔法打败魔法,让 AI 也全面接管审核流程,会不会更高效?

苹果的审核机制向来不够透明,有时候应用能否顺利过审,甚至取决于是否“碰巧”遇到一位气味相投的审核员。但换个角度看,至少“人”仍然是这道防线中最重要的一环。人的判断会出错,也会带有偏差,但在面对规则时仍保有一定的弹性。

我不希望,未来的软件生态,走向“AI 开发 -> AI 审核”的闭环。

本期内容 | 前一期内容 | 全部周报列表

原创

CDE:一次让 Core Data 更像现代 Swift 的尝试

上周的文章 中,我聊了聊 Core Data 在当下项目中的一些现实处境:它并没有消失,也仍然有其独特价值,但它和现代 Swift 项目之间的错位感却越来越明显。在本文中,我想继续顺着这个问题往下走,介绍我的一个实验性项目:Core Data Evolution(CDE)。

它不是一个取代 Core Data 的新框架,也不是要把开发者重新拉回旧技术。更准确地说,它是我面对这些错位时,给自己的一种回答:如果我仍然认可 Core Data 的对象图模型、迁移体系和成熟运行时能力,那么能不能让它在现代 Swift 项目中以一种更自然的方式继续存在下去?

近期推荐

实现平滑的 SwiftUI List 展开动画 (Expanding Animations in SwiftUI Lists)

开发者经常会遇到一个动画窘境:在动态调整 List 中某一行高度时,内容并不是平滑展开,而是伴随着明显的高度跳变。在本文中,Pavel Zak 通过几个实验,展示了为什么常见的 if 条件渲染、withAnimation 甚至 .transitionList 中都难以达到理想效果。尽管 DisclosureGroup 这种内建方案可以达到预期,但 Pavel 还是给出了一个更灵活的方案:基于 Animatable 与视图尺寸测量的实现方式,让 List 在动画过程中始终获得连续变化的高度,从而实现平滑的展开动画。

List(底层仍然是 UIKit/AppKit 的列表实现)有一个核心特点:它需要在布局阶段就拿到每一行的“确定高度”。因此,对开发者来说,不要让 List 面对结构变化,而是像 DisclosureGroup 那样,将“离散变化”转化为“连续变化”,持续提供可插值的高度值。这也是在处理动画异常时,开发者常常借助 Animatable 协议的原因。想进一步了解该协议的原理与适用场景,可以阅读我之前的一篇文章


如何更好的适配 iPadOS 的布局 (SwiftUI iPad Adaptive Layout: Five Layers for Apps That Don’t Break in Split View)

尽管苹果强化 iPadOS 多窗口能力的初衷是好的,但这也显著提升了开发者在布局适配上的复杂度。应用可能以类 iPhone、传统 iPad 全屏、Stage Manager 窗口等多种模式呈现。Wesley Matlock 指出,仅依赖 horizontalSizeClass 进行布局判断在实际环境中往往是不够的。开发者需要结合容器尺寸与 size class 构建更细粒度的 LayoutEnvironment,并在根视图中统一完成布局分支决策;同时借助 ViewThatFits 等机制,让系统基于真实约束选择最合适的界面形式,而不是由开发者预先假设设备类型。


RGB HDR Gain Map + ImageIO 中的使用陷阱 (Pitfalls and workarounds when dealing with RGB HDR Gain Map using ImageIO)

iOS 18 中引入的基于 ISO 21496-1 标准的 RGB HDR Gain Map,让开发者在处理 HDR 图像时获得了更高的表现力,但在实际应用中也更容易踩坑:尽管相关接口能够返回辅助数据字典,但在 RGB Gain Map 场景下却缺失了实际的位图数据(kCGImageAuxiliaryDataInfoData),导致后续处理无法继续。换句话说,ImageIO 在这一场景下甚至无法完整读取自身生成的内容。Weichao Deng 提出了一种混合方案:使用 Core Image 读取 Gain Map 的 CIImage,手动渲染为 Bitmap Data,补齐缺失字段后,再通过 ImageIO 写回文件。对于正在开发相机或图像处理类应用、需要处理 HDR Gain Map 的开发者来说,这篇文章或许能帮你省下不少调试时间。


Swift 社区的网络愿景 (A Vision for Networking in Swift)

Swift Ecosystem Steering Group 上周发布了一份关于网络编程的愿景文档,讨论了 Swift 网络生态当前的困境以及未来可能的发展方向。

文档指出,Swift 在网络领域存在明显的分裂:URLSession、SwiftNIO 与 Network.framework 并存,功能重叠却互不兼容,开发者往往需要在项目早期就押注某一套技术栈,且切换成本极高。与此同时,现有的大多数网络 API 都诞生于 Swift Concurrency 之前,依赖 completion handler、delegate 或响应式模式,与现代 Swift 的语言特性存在明显脱节。

文档提出的方向是构建一套分层统一的网络架构:底层共享 I/O 原语与缓冲类型,中间层复用 TLS、HTTP/1.1/2/3、QUIC、WebSocket 等协议实现,顶层提供以 async/await 和结构化并发为基础的客户端与服务端 API。已有的 swift-http-types(定义了 HTTPRequest / HTTPResponse)可以视为这一思路的早期实践。文档同时强调,SwiftNIO 和 Network.framework 不会被废弃,而是将逐步向统一的底层原语收敛。

该愿景目前正在征集社区反馈,可以在此参与


让你的 iOS 项目更适合 AI 协作 (Preparing Your iOS Codebase for AI Agents)

随着 AI agent(如 Codex、Claude Code 等)逐渐参与到实际开发流程中,问题开始从“如何使用 AI 写代码”转向“如何让代码库本身适合 AI 协作”。Hesham Salman 从工程实践的角度,系统性地探讨了这一转变。

Hesham 指出,相较于提示词,AI 更依赖显式契约。通过分层的 AGENTS.md 文档明确项目约定与行为规则,使用 Makefile 将构建、测试等操作统一为可执行入口,并通过“skills”将多步骤流程编码为可复用的执行方法,从而将原本隐性的工程知识结构化地嵌入到代码库中。

文章中有一个细节令人印象深刻:作者要求 agent 在发现未记录的约定时主动更新文档,同时加入了一条强约束——每次修改都必须让文档更短或更有用。这一自维护机制既防止文档腐化,也避免文档膨胀,是一个值得借鉴的平衡策略。


iOS Conf SG 2026 视频

2026 年 iOS Conf SG 于 1 月 21 日至 23 日在新加坡举行,来自全球的数十位开发者与内容创作者分享了各自在苹果生态开发中的经验与思考。上周,官方放出了本届的全部演讲视频。我也有幸参与了其中的一场分享,感兴趣的读者可以按需挑选观看。

工具

TaskGate:解决 Actor 重入的工具

尽管 actor 在很大程度上避免了数据竞争,但其可重入(reentrancy)特性也意味着,一些看似串行的逻辑在 await 之后可能失去原有的执行顺序,进而造成重复执行或状态不一致。

Matt Massicotte 编写的 TaskGate 正是为这类场景准备的。它提供了 AsyncGateAsyncRecursiveGate 两种机制,用来为 actor 内部的异步代码定义“临界区”,确保同一时间只有一个任务能够进入相关逻辑。与传统锁不同的是,它允许在持有 gate 的同时安全地执行异步调用。

Matt 明确指出:该库并不是用来替代良好的 actor 设计,而更像是一种在其他手段不够合适时的补充工具。库中将 gate 刻意设计为 non-Sendable,以降低跨 actor 误用的风险。如果你正在处理 actor 重入导致的状态一致性问题,或希望更深入理解 Swift 并发中的这一薄弱环节,这个库以及 Matt 在 Reddit 中的 讨论 都值得一看。


pico-bare-swift

苹果当年创建 Swift 时,对它的期待显然不只是用于开发 App,而是希望它最终成长为一门适用于不同领域、不同层次的通用语言。不过,在相当长一段时间里,Swift 在那些传统上由 C/C++ 或 Rust 主导的领域中,始终没有展现出足够的存在感。kishikawa katsumi 通过这个示例项目展示了另一种可能:借助 Embedded Swift,Swift 已经开始具备进入嵌入式开发场景的能力。

这个项目最吸引我的地方,在于它将一件原本带有明显“底层/C 语言专属”色彩的事情,组织成了一条相当清晰的学习路径。它不仅是“用 Swift 点亮一个 LED”,而是将启动代码、向量表、内存初始化、寄存器访问,以及 UART、PWM、I2C、SSD1306 OLED 等外设驱动一并纳入 Swift 的实现范围之中。某种程度上,这类项目的意义不在于“是否实用”,而在于它重新划定了 Swift 的能力边界

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

我在微信养了一天龙虾🦞,花了 20 万Token让它给我发压缩包

现在,你的微信里也能养「龙虾」了。

龙虾爆火后,在 AI 牌桌上一向低调的腾讯,罕见打出一套快拳,迅速端出三款「龙虾」,其中最值得拿上台面聊聊的,当数 QClaw——

这是腾讯电脑管家基于 OpenClaw 打造的一款本地 AI 助手,它最特别的地方在于你可以支持直接在微信与「龙虾」对话,让它帮你干活。

今天,QClaw 正式更新 v0.1.9 版本,用户可以通过小程序接收电脑端文件,同时上线了足以充当龙虾指南的「灵感广场」。

APPSO 第一时间实测了微信养龙虾,看看实际体验如何。

一只对小白友好的腾讯龙虾

QClaw 的界面长了一张大家都很熟悉的「AI 脸」:左边聊天,右边干活。为了让你最快上手,它在主界面的 C 位甩出了几个预设选项。点击「安装你的第一个 Skill」,这只龙虾就会手把手教你如何点亮它的技能树。

背靠 ClawHub 和 GitHub ,QClaw 拥有的 Skill 储备超过 5000 种。面对这么庞大的库,该怎么挑?腾讯给出的答案很直接:别挑,直接开口。你只需要用大白话描述你想干啥,它就会自动把合适的 Skill 端到你面前。

傻瓜式的交互,极大抹平了新手的学习曲线。但对喜欢掌控感的人来说,难免会有一点隐秘的焦虑——总得有个完整菜单让我看看有什么菜吧?

稍微翻找一下,你会发现它藏在设置的「技能管理」中。在这里,你能总览所有技能,甚至可以直接从 GitHub 粘贴导入。但耐人寻味的是,哪怕在这个稍显硬核的管理界面里,排在最前面的添加方式,依然是「通过对话创建」。

可以看出来的是,在决定基础体验与 QClaw 能干什么的事情上,腾讯想尽量将事情做简单——刚刚 QClaw 回复 Skill 列表的第三点,依旧在鼓励我直接告诉它想要什么样的 Skill。

微信养虾很有趣,只是这虾有点生

部署好电脑端之后,我们直奔重头戏——微信遥控。

先在主界面左下角用微信登录 QClaw。注意:目前内测仍需填写邀请码,没有邀请码的话,就算微信登录成功也是一个空壳,什么都做不了。

不过,光在电脑端完成登录,还不足以召唤出这只「龙虾」的完全体。要想真正把它装进口袋,还得进行一次关键的跨屏连线。

在界面左下角的头像旁唤醒「微信远程」,掏出手机微信扫一扫,界面会丝滑地跳转到微信里的 QClaw 客服对话框,另一头的电脑屏幕也会默契地亮起连接成功的提示。

不需要任何复杂的内网穿透或代码配置,你的微信聊天框,此刻已经正式变成了一个能随时使唤电脑干活的随身遥控器。

我相信大多数人面对这只一举一动都要花钱的龙虾(当然,目前内测期间 Token 免费),图的绝不仅是多一个代发微信的聊天搭子,而是能实打实分担工作压力、能帮我干活儿的数字员工。

对于我也是如此——尤其是当我不在办公电脑前,又急需一些文件和图片的时候。

QClaw 最大的亮点就是免去麻烦的部署,可以通过微信对话框指挥电脑上的 QClaw,而在 v0.1.9 版本,QClaw 上线小程序文件传输能力,用户可以直接通过小程序接收电脑端文件,灵活性进一步提升。

那它的实际表现如何呢?

在我的电脑下载中,有几张拍摄的样片急需放进推文中,但我此时离公司十万八千里,于是我通过客服号中的 QClaw 对话框下达指令,请 QClaw 将照片传递过来。结果——

啊?

不死心,重试一次。这次成功了,但只能算「基本成功」——从消息内容来看,QClaw 似乎只回过来了后半段,前半段被悄无声息地吞掉了。

为了搞清楚发生了什么,我火速赶回公司,看看电脑端的对话框里是怎么呈现的这次任务:

也就是说,其实第一次下达指令后,QClaw 是成功响应了,但没有顺利反馈到手机微信里的对话框中;而第二次更是提示我可以在 QClaw 小程序中随时查看,但消息却没发送全,唯一幸运的是后半部分的链接顺利递到了我的对话框中,让我至少能正常下载需要的照片。

对于工作来说,文件的任何信息都很重要,所以我打算进一步拷打一下 QClaw:

我需要的这些照片,会被 QClaw 偷偷压缩吗?小程序中保存的照片,与链接中的照片是否一致?有没有丢失 Exif 信息呢?

抱着这样的疑问,我用手机打开「QClaw 管家」小程序,照片确实秒速送达了。令人无奈的是,QClaw 自作主张地将三张照片打成了压缩包,文件不支持点击选中,也不给任何下载到本地再想办法解压的余地。

最后的结果是这份急需的资料就这样死死僵在了列表里,没有任何办法增删查改。折腾了半天,我唯一能做的,就是隔着手机屏幕和它干瞪眼。

▲ 啊?

此时一定有人提问:不是还有链接吗?人家说小程序是用来查看的,你用链接下载不就好了。

没毛病,但我用手机返回客服号对话框,重新找到下载链接时,发生的一幕让我血压暴涨——

这个链接,是用来跳转到 QClaw 管家小程序的。

当一个事情离谱到超出我意料的时候,我会非常执着地想看看它到底能离谱到什么地步。

于是我又不信邪,用电脑点击 QClaw 给我的那条下载链接。

可喜可贺——这一次没有出任何差错,文件下载下来了。不仅下载下来了,而且图片还没有任何压缩,Exif 信息也没有任何丢失。

但是我怎么就是高兴不起来呢?

让我们看看我最初是想干什么?

因为我不在办公电脑前,所以我找 QClaw 给我发文件;
QClaw 给我发到小程序里,还给我发了链接,相当周全;
小程序里是压缩包,我打不开、看不了、下不动、删不掉;
手机打开的下载链接也跳转到小程序,我打不开、看不了、下不动、删不掉;
最后只能用电脑点击下载链接,才能顺利看到文件。
……

好,可能是文件夹里三张图片对于 QClaw 这样刚蹒跚学步的龙虾来说太多了,我只留一张,再来一次。

▲ 终于成功了

在我特别叮嘱「别压缩」的前提下,成了!并且 Exif 信息没有任何丢失,大成特成!

顺带一提,刚刚这一顿操作下来,又是 20 万 Token 没有了。

灵感广场,教你怎么养龙虾

对没碰过「龙虾」的小白来说,前期的本地部署就像在徒手拼装一台发动机,费尽心思终于熬过了复杂配置,满心欢喜地准备拥抱赛博未来,迎面撞上的却只有一个光秃秃的代码框——我真不知道这玩意能干什么, 或者说我不知道它能怎么帮到我。

老天给你发了一把绝世好剑,却忘了给剑谱,而 v0.1.9 上线的「灵感广场」,刚好充当了剑谱作用。

腾讯在灵感广场中预设了 15 种任务模式。说实话,其中大部分任务并不能直观体现出龙虾的想象力,以前的大语言模型 AI 也能做到看看八字、梳理知识点框架。于是,在一众应用中,我找了一个较为本地化的操作:发票/单据智能归档。

我的电脑里刚好存放着去年大半年的发票准备报销,但直接在电脑上用预设功能实在没什么意思,我打算用微信通知 QClaw 帮我智能归类,并输出为 Excel 表格:

把电脑上下载中发票报销文件夹里的发票都帮我整理成报销明细 Excel 表格

不知道是不是我在发票报销的文件夹中根据项目分出了近十个小文件夹的原因,QClaw 执行整个指令用了约五分钟的时间,最终输出的 Excel 表格通过文字反馈给了微信客服号的对话框中,并同样附上了小程序的链接。

美中不足的是,QClaw 出现了部分发票识别不了的情况——我所有的发票都是 PDF 格式,但由拍摄转为 PDF 的实体发票识别无一例外都失败了,结果差强人意。

随后,我又用电脑端单独输入了一遍同样的指令,得到的结果保持一致——由照片转来的五张发票无法顺利识别。

打开设置看看用量统计,电脑整理发票这条指令消耗了 839,061 Token,是单条简单对话的 20 倍左右,而手机微信远程指挥的消耗则为 459,501,Token 消耗比较不稳定。

不过在折腾这个任务时,我也踩到了微信遥控 QClaw 的弊端——

你在手机微信里下发的所有指令,到了电脑端并不会根据任务自动分流,全都简单粗暴地把消息塞进了一个对话框里。:一旦你想回到电脑端复盘之前的任务进度,面对的就是一个深不见底的文字瀑布。没有标签,没有分类,你唯一能做的,就是疯狂搓动鼠标滚轮,在海量的历史记录里苦苦打捞你需要的回答。

预设任务完成得尚可,更个人、更日常的任务呢?

我打算从最简单的入手——发微信。

我请 QClaw 帮我叫女朋友起床,按道理,在 v0.1.9 版本中,QClaw 已经接入微信了,发个微信应该不是什么难事儿。但意外的是,接入微信的 QClaw,找不到我的微信联系人。

面对这种窘境,QClaw 反复尝试挣扎,在经过备注、用户名、微信号三重查找后,浪费了近 20 万 Token 的 QClaw 终于找到了问题所在:

看到问题了!微信渠道虽然启用了,但 guid 和 userId 都是空的,说明微信账号还没有完成绑定/授权。

看起来很合理,但我目前已经绑定了微信,并退出重新登陆过一次,依旧无法成功,换到手机微信客服号远程指挥电脑上的 QClaw,也依旧失败。

于是我继续追问如何填充 guid 与 userld,又花费了近 20 万 Token 的 QClaw 这样回答:

看起来头头是道,逻辑正确、方案合理,但我翻遍了设置也没有找到其中任何一个解决办法的入口,而截止本篇体验完稿时,我依旧没能叫她起床……

关掉 QClaw,读者们大概会分成两拨——乐观者会期待,悲观者会批评。

但我并不打算对一个版本号仅为 v0.1.9 的初生牛犊过于苛刻。这是一个相当年轻的版本,从产品逻辑上,能看出腾讯在尽力降低龙虾的准入门槛,但一旦触及到细分需求,它就会出现零零散散的不如意。

这很符合逻辑,易用需要大众,而生产力则天生偏向极致细分,解决这样的矛盾还需要时间。目前的 QClaw 只是呈现一个粗糙的框架,向我们掀起未来一角。

跳出 QClaw 这盘「小龙虾」,也许我们还可以有一些更大的猜想——

之前我们在文章《OpenClaw 让每个聊天软件都有机会变成微信》中提到:

当一个聊天窗口可以调用任意 agent 完成从订票、编程到数据分析的任意任务时,它已经不只是一条管道——它正在变成一个超级接口。

有意思的是,这个让全球开发者兴奋不已的叙事,对中国用户来说却充满着强烈的既视感。用一个封闭生态实现「全服务覆盖」,这不就是微信当年用小程序做过的事吗?

QClaw 在体验上的种种不如意,以及未来可以预见的权限摩擦,本质上是开放工具撞上封闭生态时的必然代价。它费尽心思想绕过的那堵权限墙,对微信自己来说,不过是底层架构里的一行代码。

第三方工具在缝隙里挣扎的每一步,对平台原生能力来说都只是举手之劳。

能力的边界,往往就是入场资格的起点。

QClaw 只是掀开了一角,让我们看到了 IM 平台向「通用交互层」进化的可能性。而真正的问题是:当微信亲自下场,把原生 Agent 融入其中,那个版本的体验会是什么样的?

想象一下,不需要邀请码,不需要跨屏连线,不需要在压缩包和跳转链接之间反复横跳——只需要打开一个你每天都在用的聊天框,说一句话,事情就办完了。

这才是那个 AI 时代真正意义上的「超级接口」。

QClaw 让我们预习了这道题,但最终交卷的人,可能另有其人。

让我有个美满旅程

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

50 岁的苹果和 51 岁的我 -- 肘子的 Swift 周报 #127

issue127.webp

50 岁的苹果和 51 岁的我

再有不到半个月,Apple 将迎来 50 岁生日。Tim Cook 也发表了一篇短文,致敬过去半个世纪的历程。不过,由于苹果一直以来始终引领潮流的形象,很多人并没有意识到它已经是 IT 产业中名副其实的元老。与它年龄相当的 IT 巨头,如今仍留在一线牌桌上的寥寥无几。

作为一个只比苹果大一岁的科技爱好者,从 Apple II 到如今的 iPhone、MacBook,苹果的产品几乎伴随我走过了大半人生。严格来说,我并不算真正的果粉——不会因为没能第一时间买到新品而遗憾,也几乎不再熬夜看发布会,更说不出新产品的具体参数。但回顾过去,在每一个人生节点上,我都会很自然地选择苹果的产品,并在近几年成为了苹果开发生态中的一员。

其实我也没有完全想明白,苹果对我持久的吸引力究竟来自哪里。是因为很早就开始使用它的产品?是它的创新、体验和气质?还是 Jobs 的人格魅力?说实话,如今的选择已经完全出于习惯和本能,就像老友间的默契,早已不需要什么特别的理由。

当然,苹果的成长之路并非一帆风顺,其间也有过低谷。但有一点必须承认:它在过去 50 年间的企业定位几乎没变——为个人和社会创造强大的工具。即便在最新一轮 AI 浪潮中,苹果看似失去了先机,但作为连接人与数字世界的“最后一厘米”的核心参与者,它仍然具备在 AI 时代留在牌桌中央的资本。毕竟,我们生活在物质世界中,需要实打实的硬件设备和个人化服务来享受技术进步的成果。

50 岁的苹果或许能给更多企业带来启示:与其模仿它“炫酷”、“创新”的外表,不如学习它的专注与坚持。成为与用户长久互相陪伴的伙伴,或许才是它成功的真正密码。

大概率再过十年,当苹果 60 岁、我 61 岁的时候,我仍然用着一台苹果电脑。

生日快乐,苹果!

本期内容 | 前一期内容 | 全部周报列表

原创

2026 年,为什么我仍在思考 Core Data

到 2026 年,Core Data 已经问世 21 年,尽管仍有不少开发者在使用它,但在今天的 Swift 项目里,它越来越像个“时代遗留”。并发得靠 perform 一层层套,模型声明堆满样板代码,字符串谓词随时等你踩坑。这篇文章不是要为 Core Data 辩护,也不是要说服新的开发者回到 Core Data。它更像是一篇问题整理:在 2026 年,为什么仍有人坚持使用 Core Data;而如果要继续使用它,我们今天真正需要解决的问题又是什么。

近期推荐

原生 AI 聊天应用 — 极速、隐私优先、100+ 专业功能

一个原生应用,100+ AI 模型,支持 Mac、iOS 和 Android。极速响应、键盘驱动、非 Electron。使用码 FATBOBMAN25 立享 25% OFF。


苹果工程师谈应用安全与内存保护 (Fortify Your App: Essential Strategies to Strengthen Security Q&A)

在苹果开发者中心举办的一场安全专题活动中,多位苹果工程师围绕应用安全与内存安全进行了近六小时的分享与问答,内容涵盖现代应用面临的安全挑战,以及 Apple 平台提供的一系列防护技术。Anton Gubarenko 将这场活动中的大量开发者问答整理成文,讨论了第三方库安全评估、UserDefaults 与 plist 数据存储的风险、Keychain 与文件保护策略、Swift unsafe API 的使用边界,以及如何在 Xcode 中启用 Enhanced Security 等能力。对于希望了解 Apple 平台安全机制与实践建议的开发者来说,这是一份信息密度很高的问答整理,其中包含不少来自苹果工程师的一手信息。


用 CLI 与 MCP 自动化配置 iOS 订阅 (Faster iOS Subscriptions with ASC CLI and RevenueCat MCP)

为应用添加订阅功能本身并不复杂,但在 App Store Connect 与 RevenueCat 两个后台之间来回配置,过程往往相当繁琐。Rudrank Riyam 介绍了一种更高效的做法:使用 ASC CLI 在终端中一次性创建订阅产品,再让 AI 代理通过 RevenueCat 的 MCP Server 自动完成 entitlements、offerings 与 paywall 的配置,从而将原本依赖控制台点击的流程迁移到 CLI + AI Agent 的自动化工作流中。


JetBrains 面向 Swift 开发者的调查 (JetBrains Swift Developers Survey)

JetBrains 最近发布了一份面向 Swift 开发者的调研问卷,邀请开发者分享当前使用的开发工具、工作流程以及在 Swift 生态中的痛点。尽管官方并未说明调研的具体用途,但社区中已经出现不少猜测:这项调查可能与 JetBrains 重新评估 Swift 开发工具支持有关。

在 JetBrains 于 2022 年宣布停止维护 AppCode 之后,Swift 开发者基本回到了以 Xcode 为核心的工具链。此次调研也引发了一些讨论——有人期待 JetBrains 重新探索 Swift tooling 的可能性,也有人认为这更可能与 Kotlin Multiplatform 或 Swift 构建工具链相关。如果你对 Swift 开发工具生态的未来方向感兴趣,不妨参与这份调查。


不依赖编译器识别 Swift Protocol 的方法 (How Well Can You Detect a Swift Protocol Without the Compiler?)

在 Swift 项目中,Protocol 几乎无处不在,但如果不依赖编译器或完整构建环境,仅通过源码文本判断一个文件是否定义或使用了协议,结果会有多可靠?Xiangyu Sun 在这篇文章中系统评估了多种检测策略,例如使用 SourceKit/LSP、SwiftSyntax AST、关键字正则匹配,以及通过 extension Foo: Barany / some 等语法信号进行启发式判断,并对这些方法的准确率与适用场景进行了比较。

文章最有意思的部分在于作者发现:简单的命名约定可以显著提升静态分析效果。如果团队统一使用 *Protocol 后缀命名协议类型(如 PaymentServiceProtocol),很多原本存在歧义的检测方法都会变得更加可靠。作者还进一步讨论了这种约定在 AI 辅助开发中的价值:通过在文件级别预分类协议文件,可以在向 LLM 提供上下文时显著减少 token 消耗,并提高分析效率,这是一个颇具启发性的视角。


迁移到 Swift Concurrency 前需要注意的细节 (What you should know before Migrating from GCD to Swift Concurrency)

从 GCD 迁移到 Swift Concurrency 并非简单的语法替换。在这篇文章中,Soumya Ranjan Mahunt 指出:Swift Concurrency 在任务调度、执行顺序以及并发语义上与 GCD 存在一些关键差异,例如 Task 的调度并不保证与 GCD 相同的 FIFO 执行顺序,而 actor 也并不是 DispatchQueue 的直接替代,其执行行为可能受到任务优先级和调度策略的影响。此外,文中还讨论了一些在实际迁移过程中容易被忽视的问题,例如 DispatchGroup 在 Swift Concurrency 中并没有完全等价的 API,以及在旧系统版本中使用 assumeIsolated 可能遇到的兼容性问题。


选择 AI Agent Skill 的九步框架 (A 9-Step Framework for Choosing the Right Agent Skill)

随着 AI Agent 在开发工作流中的应用越来越广泛,如何为 Agent 设计合适的“技能”(Skill / Tool)也逐渐成为一个新的工程问题。Antoine van der Lee 提出了一个用于判断何时应该为 Agent 创建技能的九步框架,帮助开发者在自动化能力、可维护性以及系统复杂度之间取得平衡。Antoine 指出,并非所有任务都适合直接交给 LLM,也并非所有能力都需要实现为 Agent 工具。文章从任务确定性、执行成本、可复用性以及安全性等角度出发,提供了一套相对系统的评估思路。

工具

DataStoreKit

这是一个很有意思的开源项目,由 Anferne Pineda 开发。它基于 SwiftData 的自定义 store 能力,在保留 SwiftData 上层开发体验的同时,重新实现了一套面向 SQLite 的底层存储后端,包括从 SwiftData 模型、谓词到 SQLite schema、SQL、快照与持久化历史的映射和执行。

DataStoreKit 提供了一些值得关注的特性,例如支持对数组、字典等集合类型数据进行谓词查询,底层以 JSON 形式映射到 SQLite;同时也提供了 SQL 直通能力,让开发者在 #Predicate 之外,能够直接利用 SQLite 的能力完成查询或维护操作。

这是目前为数不多、且实现深度较高的 SwiftData DataStore 自定义实践,展示了 SwiftData 作为数据表现层而非完整持久化引擎的另一种可能性。项目目前仍处于较早期阶段,API 和能力边界可能还会继续调整,但已经非常值得持续关注。


Playwright for Swift

Miguel Piedrafita 开发的 swift-playwright,将 Playwright 这套成熟的浏览器自动化能力带入了 Swift 生态。开发者可以直接使用 Swift 代码驱动 Chromium、Firefox 和 WebKit,完成页面导航、点击、输入、截图、执行 JavaScript 等常见操作,整体 API 风格也尽量贴近官方 Playwright。

从实现方式上看,它并不是重新实现一套浏览器自动化框架,而是在 Swift 侧封装了 Playwright 协议,底层依然通过 Node.js 的 Playwright driver 与浏览器通信。对于希望使用 Swift 构建测试工具、CLI,甚至 AI Agent 的开发者来说,这个项目提供了一个颇具吸引力的切入点。

活动

LET'S VISION 2026 -- Born to Create · Powered by AI

  • 👀 70+ 展商现场体验
  • 🤖 AI 创新产品 / AI Agent
  • 🥽 XR / 空间计算沉浸体验
  • 🎤 创作者与开发者分享

如果你是开发者、设计师、产品经理、创作者,还是对 AI 和未来科技感兴趣的探索者,都很值得来逛逛。

  • 📅 2026.3.28 – 3.29
  • 📍 上海 · 漕河泾会议中心

15% OFF 门票 👇

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

当我们说「文科生也能做AI」时,我们在说些什么

「文科生也可以做 AI」 「逆袭!」在中文互联网上,文科和 AI 的拉郎配,简直成了定番。

每隔一段时间,这个标签就会被贴在某个人身上,制造出一轮短暂的流量。要么是逆袭故事,要么是嘲讽素材,取决于评论区的心情。

一个标签,三种做法

最新的案例是杨天润, AI 创业者,金融出身,正在开发一个多智能体协调平台。他自称「一行代码都不会写的文科生」,搭建了一组 AI Agent,向 GitHub 上最热门的开源项目之一 OpenClaw 批量提交代码贡献。

想验证一个假设:一个完全不懂技术的人,能不能仅靠指挥 AI,就参与到顶级开源项目中去。

结果是:134 个 PR,21 个被合并,113 个被拒绝。前几个 PR 质量还算不错,被维护者认可并合并。但当他给 Agent 下了一条加速指令后,事情迅速失控——Agent 开始像流水线一样批量生产低质代码,在评论区疯狂@维护者催促审核。OpenClaw 管理员介入清理,GitHub 随后修改了 PR 提交上限规则。

黑红也是红,红过之后再黑更加是。杨天润被包装成「文科生逆袭」的代表,而他本人似乎也乐于接受这个角色。在接受品玩的采访时,他说了一句这样的话:

不懂代码反而是优势。AI 是梵高,你是个小画家,你有什么资格告诉梵高中间该用什么笔触?

细思极恐。他把「不懂底层结构」理解为一种解放:不需要知道系统在做什么,只需要告诉它你想要什么。结果就是当 Agent 开始批量刷垃圾代码时,他连发生了什么都诊断不出来,因为他根本不知道自己在操作什么。

他以为自己在指挥梵高,实际上他在盲开一辆没装刹车的车,而且根本不知道刹车在哪。

围绕这件事的讨论,也随之落入两个极端:要么「文科生也能做 AI」,要么「文科生别碰 AI」;前者是跨越鸿沟的壮举,要么是掉进鸿沟的笑话

如果我们对「文科生做 AI」的想象力只有这些,那未免太贫乏了。

Claude 为什么需要一个哲学家

我们之前写过,Anthropic 的办公室里,有一位正儿八经的文科生,深度参与了 Claude 的建设。不是测试它能不能写代码,不是检查它的数学能力,而是和它进行漫长的、关于价值观、关于措辞分寸、关于「面对不确定性应该如何表达」的对话。

Amanda Askell,苏格兰人,今年 37 岁。她的职业路径本身就是一个不太寻常的故事:在大学,她最初学的是美术和哲学,后来转向纯哲学,在牛津拿到了 BPhil,又在纽约大学拿到了哲学博士。她博士研究的是无限伦理学中的帕累托原则:当涉及无限数量的道德主体或无限时间跨度时,伦理排序应当遵循什么规则。

这听起来像是距离硅谷最远的学术方向,但她先后加入了 OpenAI 的政策团队和 Anthropic 的对齐团队。2021 年起,她成为 Anthropic「性格对齐」团队的负责人,工作重点是塑造 Claude 如何与人类对话、如何在不确定时表达立场、如何在价值观冲突中做出判断。2024 年,她入选了 TIME100 AI 榜单。《华尔街日报》描述她的日常工作是「学习 Claude 的推理模式,用长度超过 100 页的提示词来修正它的行为偏差」。据说她是这个星球上和 Claude 对话次数最多的人类。

为什么一个 AI 公司需要一个哲学家来做这件事?答案藏在一些非常具体的技术选择里。

今年 1 月,Anthropic 发布了一份长达 80 页的文件,被称为 Claude 的「宪法」。媒体关注的是文件末尾关于 AI 意识的推测——当然,老板 Dario Amodei 也话里话外「暗示」这一点。

但更值得注意的是它的底层逻辑:教 AI 理解为什么要这样做,比告诉它应该怎样做更有效。这是一个技术判断,认为内化价值比遵守规则能产出更可靠的行为,而这种判断的知识根基,来自一个学美术、学哲学的人。

Amanda 的案例回答了一个问题:被视为「无用」的学科知识,能否成为技术系统的核心能力?答案不仅是能,而且,没有她的哲学训练,Claude 的对齐问题用现有的工程方法解决不了。

被重新命名的学科

如果 Amanda 的故事说明了,某些被归为「文科」的学科训练可以是 AI 的核心能力,那么林俊旸的故事要说的是一件更重要的事:有一整个学科,一直在大模型技术栈底层运行。

林俊旸离开通义千问后,中文互联网的报道反复使用同一个说法:他有应用语言学背景。稍微传几次,这个话就变形了,变成了他是「文科生」。

这个标签和杨天润身上贴的是同一个,但其实被严重扭曲。

林俊旸学的是语言学,这是一个伞状学科,它的分支覆盖语言教学、语言政策、翻译研究,也包括计算语言学。可以说,计算语言学,就是自然语言处理(NLP)之子。

乔姆斯基在 1950 年代提出了形式语法,这个理论工具直接催生了早期 NLP 的句法分析技术;Daniel Jurafsky 和 Christopher Manning,这两位 NLP 领域被引用最多的两本教科书的作者,都是语言学出身。

▲ 乔姆斯基

换句话说,「学语言学的人去做 NLP」就像「学物理的人去做芯片设计」一样,是一条正统路径,不是跨界。

那个「意外感」完全是中国语境制造的。高考文理分科的制度惯性,把「语言学」塞进了「文科」的心智模型里。但语言学的核心方法论——形式化、统计建模、语料标注——本质上是工程思维。林俊旸在北大的合作者孙栩、苏祺,都是 NLP 方向的研究者;他 2019 年加入达摩院时进入的是 NLP 团队。这不是一个文科生误入技术领域的故事,从一开始就不是。

比「林俊旸不算文科」更值得展开的,是语言学在大模型技术栈里实际扮演的角色。它比大多数人以为的要深得多,也隐蔽得多。

比如分词。所有语言模型处理文本的第一步,是把输入切成模型能处理的基本单元。对英语来说,空格提供了天然的词边界,看起来简单。但中文里,没有空格,且每一个标点符号的用法,都可以左右句子的表达意思。

「我在北京大学读书」是切成「我/在/北京/大学/读书」还是「我/在/北京大学/读书」?这不是一个有标准答案的工程问题,它取决于你对中文词汇结构和语义单元的理解。

2024 年底有研究者专门发表论文,讨论如何优化 Qwen 模型的阿拉伯文分词效率,因为通用方案在处理这类语言时效率显著下降。Qwen 系列在多语言上的表现,不是把所有语言当英语的变体来处理,而是基于对语言间结构性差异的理解,做出的设计选择。

又比如反馈对齐。RLHF 流程中,标注员需要判断模型的两个回答哪个「更好」。这个判断听起来主观,但它背后有一套语言学已经研究了几十年的框架:语用学。

标注员在评估「好的回答」时,实际上是在判断合作原则——回答是否提供了足够但不过量的信息?会话含义——回答是否捕捉到了用户真正想问的、而不仅仅是字面上问的东西?语境适切性——同样的内容,用这种方式说在这个场景下是否得体?

「Helpful, Harmless, Honest」这套被广泛使用的对齐标准,本质上就是语用学基本原则的工程化翻译。

从林俊旸的学术轨迹中,也能看到一种非常语言学的研究风格。他主导的 OFA(One For All),2022 年发表于机器学习领域的顶级会议 ICML,至今被引用近 1500 次。这个工作的核心思路不是为每个任务搭专用方案,而是用一个足够通用的序列到序列框架,把图像生成、视觉定位、图像描述、文本分类等跨模态任务统一起来。

从 OFA 到 Qwen-VL(被引超过 2200 次),再到 Qwen2.5,以及最新的 3.5,一条清晰的线索贯穿始终:与其为每个问题发明一套专门的解法,不如找到一个足够好的通用框架,让所有问题在同一个框架里被解决。

用最少的规则,覆盖最多的现象——这正是语言学几十年来的核心追求。生成语法的全部学术野心,就是找到一套有限的规则系统,能够生成无限的语言表达。OFA 的架构哲学与此同构,为每种语言现象写一套专门规则并不现实,应该寻找一个底层框架来统一它们。

林俊旸做大模型做得好,不是因为语言学背景「也能」做 AI,而是语言学训练塑造了一种特定的学术品味,对统一性和形式化的偏好。这种品味在大模型时代,恰好是核心竞争力。

看不见的地基,看得见的需求

三个人,同一个标签,三种完全不同的结构。

杨天润不懂底层结构,把「不懂」当优势,结果失控。这是「文科生做 AI」的空壳版:标签制造了流量,但没有任何学科训练在起作用。他的故事体现的恰恰是——当「文科生」只是一个营销标签时,会发生什么。

Amanda Askell 的哲学训练构成了对齐问题的核心方法论。没有她,Claude 不是 Claude。她的故事回答的问题是,被视为「无用」的学科知识,能否成为技术系统的核心能力。答案是不仅能,而且不可替代。

林俊旸的语言学训练构成了大模型技术栈的隐性基础设施。他的「文科背景」从来不是跨界,是正统路径。他的故事回答的问题是,文科对于先进技术的贡献,到底「隐性」到了什么程度,它是不是正在变得显性。

而终极问题并不是「文科生能不能做 AI」,而是我们能否理解到一点:靠表面上的「有没有用」来评判知识和学科,已经过时了

随着大模型从追求能用好用,走向追求可靠和可控,这些被归入「文科」的学科训练,价值不是在缩小,而是在扩大。模型越强大,越需要精确的评估体系来诊断它在哪里、为什么出错,也越需要理解语言和意义的复杂性来设计更好的训练数据,越需要在对齐问题上做出有学科敏感度的判断。

「文科生逆袭」这个叙事——无论是赞美还是嘲笑——遮蔽了真正在发生的转向:看不见的地基,正在变成看得见的需求。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

打造高效易用的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 自己就知道该怎么做了。省掉重复劳动,换来稳定可预期的输出。

Macbook Neo:苹果重回校园的起点 -- 肘子的 Swift 周报 #126

issue126.webp

Macbook Neo:苹果重回校园的起点

上周,苹果推出了若干新款硬件产品。与以往的发布会不同,这次发布显得异常低调。起初我只对其中新发布的显示器感兴趣,但在看到不少数码媒体对 Macbook Neo 配置的吐槽后,也不由得多留意了这款产品。相较于其“减配”的表象,我更从其精准的定价中看到了苹果重返教育市场的决心。

十几年前,苹果还曾是教育硬件市场的重要参与者。那些在校园中使用苹果设备成长起来的学生,也有相当一部分在进入社会后顺理成章地成为苹果软硬件的长期消费者。但随着谷歌持续加大在 Chromebook 上的投入,而苹果又缺乏更具价格竞争力的产品,这一以 K12 为核心的市场逐渐被对手占据(Chromebook 曾一度拿下美国基础教育市场近 60% 的份额)。这不仅让苹果损失了一部分收入,更重要的是削弱了其在青少年群体中、围绕台式机与笔记本这种计算形态所建立的品牌亲和力。相比平板设备,笔记本在教学体验、适用场景、耐用性以及 IT 集中管理等方面依然具有明显优势。

在服务优先的今天,硬件往往与生态深度绑定。Chromebook 早早培养出一大批习惯使用 Google Docs 的年轻用户。随着年龄增长与数据的积累,即便他们日后具备购买苹果设备的能力,也很难再与苹果的服务生态形成深度绑定,更难形成真正的品牌信仰。

Neo 精准的定价改变了这一局面。599的起售价、599 的起售价、499 的教育优惠,让更多孩子有机会在学校就开始使用苹果设备、拥有 Apple ID,从而顺着苹果“预设”的轨迹,逐步购买更多产品与服务。至于被广泛批评的"减配"——A18 Pro 芯片对 K12 日常使用场景而言完全足够,它缺的不是性能,而是定位本就如此。苹果用移动端芯片换来了激进的定价空间,这是一笔算得很清楚的账。

采用订阅制的 Apple Creator Studio,同样展现了苹果希望让更多人与其生态建立长期联系的野心。对于学校而言,廉价硬件+强大的创作软件套件,构成了闭环。Macbook Neo 的硬件性能或许不算强劲,但足以在每台设备约 4–5 年的生命周期中提供稳定、可用的体验,让使用者逐步融入苹果的服务体系。从这个角度来看,MacBook Neo 更像是苹果抛向 Z 世代与 Alpha 世代的一枚“生态锚点”。

太多消费者和数码博主过于聚焦于苹果产品是否炫酷、是否有创新,却忘记了苹果的来时路——教育硬件市场深植于它的基因之中,今天的成功源于数十年前的积累,而现在它需要补上最近十几年的空缺。对于本周报的读者来说,Neo 大概率不是你的菜。但这并不妨碍它成为一款极具针对性、也颇具野心的产品——不是用来赚快钱的,而是苹果为未来二十年的生态版图所做的一次长期押注。

本期内容 | 前一期内容 | 全部周报列表

原创

跨域传递 NSManagedObjectContext 为什么在 Swift 6.2 中不再报错?真正的变化不在编译器

当同一段与并发有关的代码在旧版 Xcode 中无法通过,却在新版 Xcode 26(Swift 6.2)中顺利编译时,你第一时间会想到什么?很多人最初的判断可能是 Swift 编译器的并发分析(如 Region-Based Isolation)又进化了,但现实并没有这么简单。本文记录了我最近遇到的一次非常有意思的排查过程:从测试失败出发,通过构建最小复现用例,一步步追溯到 Core Data 的 SDK interface,最终发现,问题的关键并不完全在 Swift 编译器本身,而是框架的导入语义发生了变化——在新的 SDK 中,NSManagedObjectContext 获得了 NS_SWIFT_SENDABLE 等宏标注,使其在 Swift 中拥有了 Sendable 语义。

尽管 SwiftData 是未来苹果生态最重要的持久化框架,但作为其基础的 Core Data 并没有被苹果冷落。在过去几年中,苹果一直在默默改善其在 Swift 6 中的兼容性和并发体验,这是一个很好的现象。

近期推荐

Notepad.exe — Swift 新特性的第一个实验场

Swift 6 出了新语法?Xcode 太重,Playground 又太慢。Notepad.exe 让你在 30 秒内写代码、跑结果,专注验证想法本身。支持多版本工具链切换,集成模拟器,随开随用。


Swift 语言 2 月新动态 (What's new in Swift: February 2026 Edition)

Karen ChuDave Lester 在官方博客上整理了 2026 年 2 月 Swift 社区的生态动态。内容不仅涵盖了 Swift 在 FOSDEM(全球最大开源会议)上的活跃表现,还推介了多项重磅的开源进展与 Swift Evolution 提案。其中的 SE-0506 尤为让我惊喜。该提案为 withObservationTracking 增加了 Options 参数,开发者现在可以精确控制是观察变化前(willSet)、变化后(didSet)还是对象的生命周期(如 deinit)。并且通过 withContinuousObservationTracking 无需再手动递归注册,即可实现稳定、自动循环的连续事件追踪。

SE-0506 提案的通过意义重大。它不仅完美补齐了状态追踪的时机控制和连续性能力,更标志着 Swift 原生的 Observation 已经彻底成熟——它不再仅仅是 SwiftUI 的“专属附庸”,而是真正蜕变为了 Swift 语言中足以应对各种工业级、高性能状态流调度的核心基础设施。


写在 2026 年的 macOS 输入法开发规范

vChewing 唯音输入法 的作者 ShikiSuen 基于多年深耕 macOS 输入法的开发经验,全面梳理了 InputMethodKit (IMK) 的历史包袱,以及它在 Swift 6 严格并发检查下暴露出的种种痛点。文章深入探讨了 NSConnection 的命名规范、启用沙盒的必要性、MainActor 隔离冲突,以及高频中英切换(CapsLock)导致的 ARC 拥堵、macOS 26 Liquid Glass 机制下 NSWindow 运存不释放等棘手问题。面对苹果“上古框架”与现代 Swift 并发模型的碰撞,作者没有停留在抱怨上,而是提出了一套像“风险控制模型”一样的工程规范——将控制器退化为纯转发层、把业务逻辑剥离到弱引用 Session、使用运存自查自尽、彻底避开 IMKCandidates 等。

可贵的是,ShikiSuen 基于上述思路开发并开源了 IMKSwift 库。它为 Swift 6+ 提供了 @MainActor 完整隔离的 IMKInputSessionController 基类,完美覆盖了原生 IMKInputController 的并发地雷区。如果你需要开发 macOS 桌面端应用或输入法,这个库能让你无需手动 Hack,就能写出类型安全、无 data-race 警告的现代代码,非常值得学习与使用。


SwiftUI 的洋葱式架构:Swift Effects 实践 (SwiftUI, Swift Effects: A Beautiful Onion Architecture)

在 SwiftUI 中处理数据加载状态几乎是每个应用都会面对的问题:loadingloadedfailed 三种状态往往伴随着网络请求、缓存、日志记录等副作用逻辑,很容易让视图代码逐渐变得臃肿。Salgara 在本文中提出了一种类似 Onion Architecture 的思路:通过 ViewState + Effect Handlers 将 Fetch、缓存、日志等副作用拆分为多个可组合层级,并利用 AsyncSequence 与可注入的 Effect Handler 驱动状态变化,使 UI 仅根据状态进行渲染。这样一来,视图保持纯粹,而数据获取与副作用则沿着一条清晰的“Effect 管道”逐层流动。同时,这种结构也天然具备良好的可测试性——测试代码可以直接拦截并模拟数据源返回值,从而验证完整的状态转换流程。

Salgara 坦言,这种架构目前仍然是实验性的:原型优先,并尝试将一切视为视图(Everything as a View)。随着越来越多开发者从不同角度思考并尝试构建更符合 SwiftUI 特性的架构,这类探索不仅可能让 SwiftUI 本身受益,也有机会反过来丰富整个声明式编程范式,而不再只是复制其他 UI 框架的既有实践。


Spec-Driven Development:当 AI 写代码之后

随着 Cursor、Claude Code 等 AI 编程智能体(Agent)的普及,开发者们正面临一个新的痛点:当 AI 能在几分钟内跨越几十个文件生成上千行代码时,人类该如何有效审查?又该如何应对 AI 在长流程中逐渐出现的“上下文遗忘(Context Decay)”与幻觉问题?为此,一种新的开发范式正在逐渐成形:Spec-Driven Development(SDD)。在这一模式下,开发者的主要任务不再是直接编写代码,而是定义清晰的规格(Spec),再由 AI 根据这些规格生成实现。

Snow 通过四篇文章系统梳理了这一思路:从 “Vibe Coding” 的局限出发,介绍以规格为核心的开发流程,并进一步探讨规格在未来软件工程中的角色——代码或许不再是项目的中心,而只是规格的衍生物。

在 AI 逐渐承担实现细节的时代,软件工程的重心或许正在从“写代码”转向“表达意图”。SDD 尝试在人类的模糊意图与 AI 的无差别生成之间,建立一层强有力的约束。


为 SwiftUI Preview 构建一个 Mini Linker (Building a Mini Linker for SwiftUI Previews)

在 Xcode 26.3 的 mcpbridge 提供的众多工具中,RenderPreview 可以直接返回 SwiftUI Preview 的截图,方便 AI 进行分析。对于暂时无法使用 Xcode 26.3 mcpbridge 的开发者,Hesham Salman 在本文中介绍的思路以及配套工具,同样可以实现类似的能力。其技术亮点在于利用 SwiftSyntax 构建声明依赖图,再通过 BFS 找出当前 Preview 真正需要的最小源文件集合,从而避免编译整个 App Target 带来的构建等待。

本文的精华在于思路:利用 SwiftSyntax + BFS 快速定位 Preview 依赖的代码片段。过去 SwiftSyntax 的使用门槛较高,但在 AI 辅助开发逐渐普及的今天,它正逐渐成为理解代码结构的重要基础设施。即便你不像 Hesham Salman 那样熟练掌握该工具,了解其基本能力后,也可以借助 AI 将类似思路落地——而这类工具在过去往往只属于少数熟悉编译器工具链的开发者。


Swift 的规模化实践:TelemetryDeck 的分析服务 (Swift at scale: building the TelemetryDeck analytics service)

很多人讨论 Swift on Server 时,关注点往往停留在“能不能用”,而 TelemetryDeck 给出的则是一个更实际的答案:不仅能用,而且已经支撑起一项面向开发者、每月处理超过 1600 万用户数据的 analytics 服务。Daniel Jilg 在这篇文章中回顾了团队为何选择 Swift + Vapor 构建后端,并分享了不少来自生产环境的经验:例如如何借助 Codable 简化 API 编解码与校验、为什么应让 DTO 更贴近 controller、以及为何缓存 TTL、API 版本管理和错误监控这些“老生常谈”,在规模化的生产环境中往往才是真正的护城河。


是时候告别 SwiftGen 了吗? (Get Rid of Your SwiftGen Dependency)

很长一段时间,开发者需要依赖类似 SwiftGen 这样的工具来解决 Apple 资源系统中的一个老问题:资源访问是字符串类型(stringly-typed)。无论是 localization key、图片名称还是颜色资产,一旦拼写错误,往往只能在运行时才会暴露问题。Asser Osama 指出,随着 String Catalog(.xcstrings)与 Asset Catalog Symbols 的引入与逐步完善,Xcode 已经能够在编译阶段自动生成资源符号,这种原生能力在不少现代项目中或许已经足以替代 SwiftGen。

需要说明的是,“移除依赖”的前提是项目完全运行在标准的 Xcode 生态中。Xcode 的符号生成属于构建系统内部机制,而不是 Swift 编译器或 Swift Package Manager 的能力——这意味着对于使用 Bazel、Buck 等非标准构建系统的团队来说,SwiftGen 仍然可能是更可移植、更可控的选择。

工具

SwiftUI Agent Skill

Paul Hudson 编写的 SwiftUI Agent Skill,旨在帮助开发者编写更智能、更简洁、更现代的 SwiftUI 代码。该项目发布仅两天便获得了 1k+ Star。

在过去几周中,本周报已经推荐了不少知名开发者编写的各类 Skill。尽管这些 Skill 都凝聚了作者的经验,但我仍不建议开发者直接“拿来即用”。至少应在采用前完整阅读一遍:Skill 更像是作者对自己数十甚至上百篇文章经验的提炼,而不是可以直接替代思考的“最佳实践”。开发者在理解其背后的设计思路后,再根据自己的开发习惯与项目需求进行取舍,这样才能更大地发挥它们的价值。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

❌