普通视图
美团发布原生多模态 LongCat-Next:当视觉和语音成为AI的母语
用 3100 个数字造一台计算机
你有没有想过,一台计算机最少需要什么?
不是说你桌上那台——那个有几十亿个晶体管、跑着操作系统和浏览器的庞然大物。我说的是最本质的那个东西:能算数、能画画、能放音乐、能响应你的键盘和鼠标。
答案可能会让你意外:一个数组就够了。
Little Virtual Computer 是一台用 TypeScript 写的虚拟计算机,原作者是 jsdf。我在他的基础上做了不少重构和优化——把代码拆分成了清晰的模块结构,加了音频系统、断点调试、内存追踪、中英文切换等功能。3100 个内存槽位,23 条指令,你可以在上面写汇编程序,画像素,甚至播放一首 Chocolate Rain。打开链接就能玩,不用装任何东西。
接下来聊聊拆解和重构这台计算机的过程中,那些让我觉得"原来如此"的时刻。
"硬件"就是一行代码
这台计算机的内存,就是这行:
static ram: number[] = new Array(3100).fill(0)
3100 个数字,所有东西都住在里面——变量、程序、输入设备、屏幕、声卡:
| 地址 | 用途 |
|---|---|
| 0 - 999 | 工作内存(变量) |
| 1000 - 1999 | 程序代码 |
| 2000 - 2051 | 键盘、鼠标、随机数、时钟 |
| 2100 - 2999 | 屏幕(30x30 像素) |
| 3000 - 3008 | 声卡(3 个通道) |
这就是"内存映射 I/O"。真实计算机里,显卡有自己的显存,声卡有自己的缓冲区,键盘通过中断传递信号。但在这里,一切都是内存地址。想在屏幕左上角画一个红色像素?往地址 2100 写个 2。想让扬声器发出正弦波?往地址 3001 写频率,地址 3000 写 3。
第一次把重构后的代码跑起来,盯着屏幕上亮起的那个像素,我突然理解了一件事:CPU 不需要"知道"什么是屏幕。它只是往一个地址写了个数字,恰好有人在监听那个地址。 输入输出不需要特殊的指令,读写内存就是一切。
CPU 其实在做一件很无聊的事
读原作者的 CPU 代码时,我以为会很复杂。结果核心逻辑是这样的:
static step(trace: boolean = true) {
if (trace) Memory.beginTrace(); // 需要调试时才追踪
const opcode = this.advanceProgramCounter(); // 从内存读一个数
const instructionName = this.opcodesToInstructions.get(opcode); // 数字变指令名
const operands = instruction.operands.map(() => this.advanceProgramCounter()); // 再读几个数当参数
instruction.execute.apply(null, operands); // 执行
if (trace) this.lastStepTrace = Memory.endTrace();
}
程序计数器从地址 1000 开始。读一个数,往前走一步。读到 9010?那是 add,再读三个数当参数,加一下,写回去。然后继续读下一个。没有流水线,没有分支预测,没有乱序执行。一个 while 循环,一直读数字、执行、读数字、执行。
这就是冯·诺依曼架构的全部:程序和数据住在同一片内存里,CPU 按顺序取指令执行。 你桌上那台电脑的 CPU,不管它有多少核、多少级缓存,本质上也在做同样的事——只是快了几十亿倍。
23 条指令够写一个游戏吗
一开始觉得不够。23 条指令,连函数调用都没有,能干什么?
结果发现,不只是够了,还能写出让人意外的东西。这 23 条指令分成五类:
搬运数据(5 条)—— 把值从一个地址复制到另一个,或者写入一个常量。还有两条指针操作,让你可以"地址 A 里存着地址 B,去 B 里取值"——间接寻址,这是实现数组遍历的关键。
算术(10 条)—— 加减乘除取模,每种都有两个版本:两个地址相加,或者一个地址加一个常量。add_constant counter 1 counter 就是 counter++。
比较(2 条)—— 比较两个值,结果是 -1、0 或 1。没有布尔值,没有大于小于等于,就一个三态数字。刚开始觉得别扭,后来发现这样反而更灵活。
跳转(5 条)—— jump_to 无条件跳转,branch_if_equal 条件跳转。没有 for 循环?跳回去就是循环。没有 if-else?跳过去就是 else。
系统(3 条)—— data 嵌入原始数据,break 暂停调试,halt 终止。
用这些东西,能写出画板程序、弹球、乒乓球游戏,甚至音乐播放器。
从文本到数字
手动往内存里填操作码太痛苦了,所以需要一个汇编器。你写这样的文本:
define counter 0
define limit 10
copy_to_from_constant counter 0
Loop:
add_constant counter 1 counter
branch_if_not_equal_constant counter limit Loop
halt
汇编器把它变成内存里的一串数字:9001 0 0 9011 0 1 0 9104 0 10 1003 9999。
过程本身很有启发性。define 给地址起名字,Loop: 标记跳转目标。汇编器用经典的两遍扫描:第一遍收集所有标签的地址(这样你可以先 jump_to SomeLabel,后面再定义 SomeLabel:),第二遍把指令名替换成操作码,把标签和变量名替换成数字,逐个写入程序内存。
所谓"编译",最原始的形态就是这样——把人能读的东西翻译成机器能读的数字。
900 个像素的屏幕
30x30,900 个像素。听起来少得可怜。
但当你亲手用汇编一个像素一个像素地画出一个弹跳的小球时,你会对"像素"这个词产生全新的理解。每个像素就是一个内存地址,颜色就是 0 到 15 的一个数字。像素地址 = 2100 + y * 30 + x。16 种颜色:黑、白、红、绿、蓝、黄、青、品红、银、灰、栗、橄榄、深绿、紫、蓝绿、海军蓝。
渲染做了分场景优化:慢放模式下追踪"脏像素",只更新被写过的像素,被写入的像素还会短暂闪白,让你看到程序正在画什么——慢放下看着像素一个一个亮起来,有种看延时摄影的感觉。全速模式则跳过逐像素追踪,直接全量重绘,因为每帧都有大量像素变化,追踪反而是浪费。
用内存地址弹钢琴
音频部分是我最喜欢的设计。三个独立的振荡器通道,每个通道就是三个连续的内存地址:波形、频率、音量。
地址 3000: 波形 (0=方波, 1=锯齿波, 2=三角波, 3=正弦波)
地址 3001: 频率 (值 / 1000 = Hz)
地址 3002: 音量 (0-100)
往这几个地址写数字,声音就出来了。改个数字,音调就变了。
内置的 ChocolateRain 程序用两个通道演奏了一首完整的曲子。音乐数据全部用 data 指令嵌入在程序里——本质上就是一个大数组,记录着"第几拍、哪个通道、什么频率、多大音量"。程序读取当前时间,算出现在是第几拍,然后去数组里找对应的音符,写入音频内存。
一首歌,就是一个按时间索引的数组。
调试器:这才是重点
说实话,这台虚拟计算机最有价值的部分不是 CPU,不是显示器,不是音频——是调试器。
点"单步",程序计数器往前走一步。你能看到它读了哪个地址(蓝色高亮),写了哪个地址(橙色高亮)。设个断点,程序跑到那里自动停下来。把速度拉到慢放,看着弹球程序一帧一帧地擦掉旧位置、算出新位置、画上新像素。
我见过很多人学编程时卡在"不知道程序在干什么"。代码写完,跑起来,结果不对,然后就懵了。这台计算机的调试器让一切都暴露在外面:每一步读了什么、写了什么、程序计数器在哪里。没有黑箱,没有抽象层,你看到的就是全部。
六个程序,六种"原来如此"
内置的六个示例程序,每个都在教一件事:
Add —— 4 + 4 = 8。三行代码,结果存在地址 2。这是"指令怎么工作"的最小演示。
RandomPixels —— 用一个指针从地址 2100 扫到 2999,每个位置写一个随机颜色,然后从头再来。满屏闪烁的彩色像素,其实只是一个循环在往内存里写数字。
Paint —— 屏幕顶部一行是 16 色调色板,点击选色,然后在画布上画。鼠标位置就是一个内存地址里的数字,点击就是另一个地址从 0 变成 1。
BouncingBall —— 白色小球弹来弹去。用 Date.now() 控制帧率,每 60ms 更新一次位置,碰到边界就反转方向。这是"游戏循环"的最小实现。
MiniPong —— 乒乓球。两个挡板,一个球,碰到挡板反弹,错过就重置。这是最复杂的示例,用到了几乎所有指令。读完它的代码,你会对"游戏不过是一堆条件判断"有切身体会。
ChocolateRain —— 用汇编写的音乐播放器。理解这个程序怎么工作,就理解了数据驱动编程的本质。
重构与实现细节
原作者 jsdf 的实现是一个完整的单体,功能齐全但耦合度较高。我把它拆成了独立模块——CPU、内存、显示器、音频、输入、汇编器——通过内存这个"总线"连接,加了 TypeScript 类型系统。
拆的过程本身就是一次学习。当你必须决定"这个职责属于 CPU 还是属于 Memory"的时候,你对计算机架构的理解会变得非常具体。
架构
项目分成两个独立的 bundle:
src/index.ts → dist/computer.module.js (核心计算机)
src/simulator.ts → dist/simulator.module.js (模拟器 UI)
index.ts 初始化所有硬件组件,返回一个 Computer 接口对象——这是两层之间唯一的契约。模拟器只通过这个接口操作计算机,不直接碰内部类。换掉整个计算机实现,只要接口不变,模拟器照常工作。
几个有意思的实现决策
内存布局用 const enum——MemoryPosition 定义所有地址常量,编译后直接内联为数字,零运行时开销。改一个数字,整台计算机的内存布局就变了。这就是"硬件规格"。
指令是数据驱动的——每条指令是一个对象,包含名称、操作码、操作数描述和执行函数。operands 数组不只是文档——汇编器用它验证操作数数量,调试器用它显示操作数含义。一份数据,三个用途。
流程控制指令直接改程序计数器——jump_to 的 execute 就是 CPU.programCounter = labelAddress。这形成了循环依赖(CPU → instructions → CPU),更"干净"的做法是把 CPU 状态作为参数传入,但在这个规模的项目里,简单直接比架构纯洁更重要。
性能:在不同场景下做不同的事
性能优化的核心思路不是"让代码更快",而是"在不同场景下做不同的事"——和真实系统的优化思路一样。
全速模式用帧预算策略:用 performance.now() 在每帧 14ms 的预算内尽量多跑 CPU 周期(留 2ms 给浏览器渲染和 GC),用 requestAnimationFrame 和屏幕刷新率同步。同时跳过内存追踪和调试面板更新,显示器切换到全量重绘。
慢放模式每次只执行一条指令,开启内存读写追踪,更新所有调试面板,显示器用脏像素增量重绘。
音频也做了状态缓存——用 state 对象记录上一次的参数值,只在值真正变化时才调用 Web Audio API,避免每帧 9 次无意义的 API 调用。CPU 停止时只需静音所有通道然后立即返回。
其他细节:内存重置用 Array.fill(0) 替代 for 循环;endTrace() 复用同一个对象避免每周期分配新数组;显示器用预计算的 Uint8Array 颜色查找表,位移 << 2 代替乘法索引;程序内存视图用虚拟滚动,只渲染可见区域 ± 10 行。
最后
折腾这台计算机的过程中,我反复体会到一件事:我们日常使用的那些抽象——变量、循环、函数、屏幕、声音——在最底层都是同一个东西:往一个地址读一个数字,或者写一个数字。
3100 个数字,23 条规则。这就是一台计算机的全部。
不信的话,打开试试:wsafight.github.io/little-virt…
点"单步",看看你的程序在做什么。
重构版源码:github.com/wsafight/li…
每日一题-区间乘法查询后的异或 I🟡
给你一个长度为 n 的整数数组 nums 和一个大小为 q 的二维整数数组 queries,其中 queries[i] = [li, ri, ki, vi]。
对于每个查询,按以下步骤执行操作:
- 设定
idx = li。 - 当
idx <= ri时:- 更新:
nums[idx] = (nums[idx] * vi) % (109 + 7) - 将
idx += ki。
- 更新:
在处理完所有查询后,返回数组 nums 中所有元素的 按位异或 结果。
示例 1:
输入: nums = [1,1,1], queries = [[0,2,1,4]]
输出: 4
解释:
- 唯一的查询
[0, 2, 1, 4]将下标 0 到下标 2 的每个元素乘以 4。 - 数组从
[1, 1, 1]变为[4, 4, 4]。 - 所有元素的异或为
4 ^ 4 ^ 4 = 4。
示例 2:
输入: nums = [2,3,1,5,4], queries = [[1,4,2,3],[0,2,1,2]]
输出: 31
解释:
- 第一个查询
[1, 4, 2, 3]将下标 1 和 3 的元素乘以 3,数组变为[2, 9, 1, 15, 4]。 - 第二个查询
[0, 2, 1, 2]将下标 0、1 和 2 的元素乘以 2,数组变为[4, 18, 2, 15, 4]。 - 所有元素的异或为
4 ^ 18 ^ 2 ^ 15 ^ 4 = 31。
提示:
1 <= n == nums.length <= 1031 <= nums[i] <= 1091 <= q == queries.length <= 103queries[i] = [li, ri, ki, vi]0 <= li <= ri < n1 <= ki <= n1 <= vi <= 105
我用 AI 撸了个开源"万能预览器":浏览器直接打开 Office、CAD 和 3D 模型
最近一直在深耕 AI Agent 与大模型应用,比如 JitKnow AI 知识库、JitWord协同AI文档、Pxcharts 超级表格,同时也持续在给大家分享 GitHub 上真正能落地、能解决实际问题的优质AI开源项目。
两周前发布了我们开源的文档预览SDK——jit-viewer。
![]()
目前在npm上已有 2.1k 的下载量,我们也在持续更新迭代,满足更多开发者的需求。
github:github.com/jitOffice/j…
在 AI Coding的帮助下,我加速了迭代频率,今天很高兴和大家分享Jit-Viewer最新版本 V1.3.0.
什么是 Jit-Viewer
![]()
简单来说,它是一个纯前端的文件预览引擎。不需要后端转换服务,不需要安装任何插件,几行代码就能让浏览器具备"专业软件"的预览能力。
![]()
过去我们 preview 文件,要么调用微软/Google 的在线接口(有隐私风险),要么自建转换服务(服务器成本高)。
jit-viewer 的思路很直接:把解析能力搬到浏览器端。下面我就和大家分享一下最新版本的更新内容。
1. 支持CAD文件预览功能
![]()
事情的起因很简单:工程团队在处理设计稿交付时,总是要在微信里发"麻烦安装个 CAD 看图软件"或者"这个 3D 模型我截图给你"。
作为一位写过无数款文档编辑器、多维表格的开发者,我突然意识到——为什么我们不能在浏览器里直接预览这些文件?
没有安装包,没有兼容性问题,打开链接就能看。这不应该是 2026 年的标配吗?
于是借助 AI, 我在 Jit-viewer sdk中支持了CAD文件的预览。
目前线上已提供demo测试,大家也可以体验测试一下。
2. 支持3D文件预览功能
![]()
3D模型预览我们开放了很多能力,比如自动旋转3D模型,对3D模型进行旋转,截图,环境渲染器配置等,基本上开箱即用,开发者不需要关注复杂的3D空间知识,只需要按照我们api文档提供的信息配置,即可实现专业的3D模型预览功能。比如你想在web系统中预览3D商品图,手动调整模型渲染方式,都是用轻松用Jit-Viewer 来实现。大家另一个比较关注的问题可能是性能问题,这里我也做了性能优化的方案:
- WebAssembly 承担重计算:CAD 的几何解析、3D 模型的三角化都在 WASM 中完成,避免阻塞主线程
- 流式加载:大模型支持 LOD(细节层次)加载,先展示低精度轮廓,再逐步细化
- Worker 多线程:解析和渲染分离,UI 永不卡顿
3. 视频预览支持完全可控的视频播放控件
![]()
我基本上重写了视频播放器,隐藏了video原生的视频播放控件,利用js api,重写了一个完全可控的视频播放 API 接口。
大家可以通过编程式来控制视频的播放,同时还能配置式控制播放控件的显示逻辑:
![]()
那么最近的迭代,有哪些应用场景呢?
jit-viewer 不只是用来"打开文件",在我们实际业务中,它解决了几个实际的痛点:
场景 1:设计评审系统
- 设计师上传 CAD 图纸,产品经理和开发直接在浏览器标注尺寸,无需安装 AutoCAD
- 支持测量工具(距离、角度、面积),数据实时同步到多维表格
场景 2:3D 电商展示
- 用户上传 3D 模型,自动生成 360° 预览,替代传统的图片轮播
- 支持爆炸图动画,展示产品内部结构
场景 3:BIM 轻量化查看
- 建筑信息模型在浏览器端轻量化展示,现场工程师用手机就能查看管线碰撞
场景 4:制造业协同
- 供应商和客户之间传递 3D 模型,不再担心"你用的 SolidWorks 版本和我不兼容"
优缺点分析(客观总结,方便大家参考评估)
✅ 优势:
- 零服务端成本:纯前端方案,不需要维护昂贵的文件转换服务器
- 隐私安全:文件不上云,本地解析,适合涉密图纸
- 极致体验:打开即看,无需等待"转换中"的 loading
- 插件化架构:按需加载,不用 CAD 功能就不加载 2MB 的 WASM 文件
❌ 局限:
- 超大文件限制:超过 500MB 的 CAD 文件还是建议用桌面软件,浏览器内存有限
- 复杂特性缺失:CAD 的图层编辑、3D 的复杂材质节点暂时不支持(仅预览)
- 移动端性能:3D 模型在低端手机上帧率可能下降,建议开启简化模式
写在最后:独立开发者的 vibe coding 感悟
作为一个连续创业者,我越来越确信:AI 不是替代开发者,而是让独立开发者有了对抗大厂的武器。
jit-viewer 的 CAD 解析模块,如果让我手写 C++ 几何算法,可能需要半年。但在 AI 辅助下,我花了两周就把 OpenCascade 移植到了 WebAssembly。剩下的时间,我可以专注在产品设计和开发者体验上。
这也是我开源这个项目的初衷——降低技术门槛,让更多人能做出专业的工具。
如果你在做 PLG(产品驱动增长)的 SaaS 工具,或者有文件预览的需求,欢迎试试 jit-viewer。
遇到问题直接提 Issue,我会亲自回复(没错,目前 issue 响应速度还在 2 小时内 ~)。
github:github.com/jitOffice/j…
git commit
# Git Commit Message 规范
## 1. 基本格式
```aiignore
<type>(<scope>): <subject>
<body>
<footer>
```
## 2. Type 类型
| 类型 | 描述 |
| --- | --- |
| feat | 新功能 (feature) |
| fix | 修复 bug |
| docs | 文档变更 |
| style | 代码格式变更(不影响代码运行) |
| refactor | 重构(既不是新增功能,也不是修复 bug 的代码变动) |
| test | 增加测试 |
| chore | 构建过程或辅助工具的变动 |
| perf | 性能优化 |
| ci | CI/CD 相关变更 |
| build | 构建系统或外部依赖变更 |
| revert | 撤销之前的 commit |
## 3. Scope 范围(可选)
表示 commit 影响的范围,如:
- `api`
- `ui`
- `auth`
- `database`
- `config`
## 4. Subject 主题
- 使用祈使句、现在时态
- 首字母小写
- 结尾不加句号
- 不超过 50 个字符
## 5. Body 正文(可选)
- 解释“是什么”和“为什么”,而不是“怎么做”
- 每行不超过 72 个字符
- 与 `subject` 用空行分隔
## 6. Footer 页脚(可选)
- 记录 breaking changes
- 关闭 issues
## 7. 语言
- 尽量使用中文
## 8. 示例
### 简单示例
```text
feat: 添加用户登录功能
fix(auth): 修复密码验证逻辑错误
docs: 更新 API 文档
style: 格式化代码缩进
refactor(api): 重构用户服务接口
```
### 完整示例
```text
feat(shopping cart): 添加商品到购物车功能
用户现在可以通过点击“添加到购物车”按钮将商品添加到购物车。
这个功能包括:
- 商品数量选择
- 库存验证
- 价格计算
Closes #123
```
-
Linux Tips, Tricks and Tutorials on Linuxize
- env Command in Linux: Show and Set Environment Variables
env Command in Linux: Show and Set Environment Variables
When you need to run a program with a different set of variables, test a script in a clean environment, or write a shebang line that works across systems, the env command is the right tool. It prints the current environment, sets or removes variables for a single command, and can even start a process with no inherited variables at all.
This guide covers how to use the env command with practical examples for everyday tasks.
env Syntax
env [OPTIONS] [NAME=VALUE]... [COMMAND [ARGS]]When called without arguments, env prints every environment variable in the current session, one per line. When followed by NAME=VALUE pairs and a command, it runs that command with the specified variables added or changed without affecting the current shell.
Print All Environment Variables
The simplest use of env is printing the full environment:
envSHELL=/bin/bash
USER=john
HOME=/home/john
LANG=en_US.UTF-8
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
TERM=xterm-256color
XDG_SESSION_TYPE=tty
EDITOR=vim
Each line is a KEY=value pair. This is identical to what printenv shows with no arguments. The difference between the two commands becomes clear when you start passing options: printenv is designed for inspecting variables, while env is designed for modifying the environment before launching a program.
Print Variables with NUL Separator
By default, env separates each variable with a newline character. If variable values contain newlines themselves, the output becomes ambiguous. The -0 (or --null) option ends each entry with a NUL byte instead:
env -0 | tr '\0' '\n' | head -5This is useful when piping environment data into tools that expect NUL-delimited input, such as xargs
with its -0 flag.
Run a Command with Modified Variables
The most practical feature of env is launching a command with variables changed for that command only. Place NAME=VALUE pairs before the command:
env LANG=C sort unsorted.txtThis runs sort with LANG set to C, which forces byte-order sorting regardless of the system locale. The current shell’s LANG value stays unchanged after the command finishes.
You can set multiple variables at once:
env DB_HOST=localhost DB_PORT=5432 python3 app.pyThe application sees DB_HOST and DB_PORT in its environment, but those variables do not persist in your shell session after the process exits.
VAR=value command syntax (for example, LANG=C sort unsorted.txt). The env command is useful when you need additional options like -i or -u, or when you want to be explicit about environment manipulation in scripts.Run a Command in a Clean Environment
The -i (or --ignore-environment) option clears the entire inherited environment before running the command. Only the variables you explicitly set on the command line will be present:
env -i bash -c 'env'PWD=/home/john
SHLVL=1
_=/usr/bin/env
The output shows almost nothing. The shell itself sets a few internal variables (PWD, SHLVL, _), but everything the parent shell normally passes along (PATH, HOME, LANG, USER) is gone.
This is useful for testing whether a script depends on variables it does not set itself. You can combine -i with explicit variables to create a minimal, controlled environment:
env -i HOME=/home/john PATH=/usr/bin:/bin bash -c 'echo $HOME; echo $PATH'/home/john
/usr/bin:/bin
A bare - (hyphen) works as a shorthand for -i:
env - PATH=/usr/bin bash -c 'echo $PATH'Unset a Variable for a Command
The -u (or --unset) option removes a specific variable from the environment before running the command:
env -u EDITOR vimThis launches vim without the EDITOR variable in its environment. Other variables remain untouched. You can unset multiple variables by repeating -u:
env -u LANG -u LC_ALL python3 script.pyThe difference from -i is that -u is surgical: it removes only the named variables and leaves everything else in place.
Change Directory Before Running a Command
The -C (or --chdir) option changes the working directory before executing the command:
env -C /var/log cat syslog | head -3This is equivalent to running cd /var/log && cat syslog, but without affecting the current shell’s working directory. It is a convenient way to run a command in another directory from within a script or one-liner.
-C option requires GNU coreutils 8.28 or later. Check your version with env --version.Using env in Shebangs
One of the most common uses of env is in shebang lines at the top of scripts:
#!/usr/bin/env bash
echo "Hello from Bash"When the kernel encounters #!/usr/bin/env bash, it runs /usr/bin/env with bash as its argument. env then searches the PATH for the bash executable and runs it. This is more portable than hardcoding #!/bin/bash, because bash is not always located in /bin on every system (for example, on FreeBSD it is typically at /usr/local/bin/bash).
The same pattern works for other interpreters:
#!/usr/bin/env python3#!/usr/bin/env nodeOn systems with GNU coreutils 8.30 or later, you can pass arguments to the interpreter using the -S (split string) option:
#!/usr/bin/env -S python3 -uWithout -S, the kernel treats python3 -u as a single argument. The -S option tells env to split the string into separate arguments before executing.
env Options
-
-i,--ignore-environment- Start with an empty environment -
-u NAME,--unset=NAME- Remove NAME from the environment -
-C DIR,--chdir=DIR- Change working directory to DIR before running the command -
-0,--null- End each output line with a NUL byte instead of a newline -
-S STRING,--split-string=STRING- Split STRING into separate arguments (useful in shebangs) -
-v,--debug- Print verbose information for each processing step -
--block-signal=SIG- Block delivery of the specified signal to the command -
--default-signal=SIG- Reset signal handling to the default for the command -
--ignore-signal=SIG- Set signal handling to ignore for the command
Quick Reference
For a printable quick reference, see the env cheatsheet .
| Command | Description |
|---|---|
env |
Print all environment variables |
env -0 |
Print variables with NUL separator |
env VAR=value command |
Run a command with a modified variable |
env -i command |
Run a command in a clean environment |
env -i VAR=value command |
Run a command with only the specified variables |
env -u VAR command |
Run a command with a variable removed |
env -C /path command |
Run a command in a different directory |
#!/usr/bin/env bash |
Portable shebang line |
#!/usr/bin/env -S python3 -u |
Shebang with interpreter arguments |
FAQ
What is the difference between env and printenv?
Both commands print environment variables when called without arguments. The difference is in their purpose: printenv is a read-only inspection tool that can print individual variables by name (printenv HOME). env is designed to modify the environment and run commands. Use printenv when you need to check a value, and env when you need to change the environment for a process.
What is the difference between env and export?export
is a shell built-in that adds a variable to the current shell’s environment permanently (until the session ends or you unset it). env sets variables only for the duration of a single command and does not affect the current shell. If you need a variable to persist for all subsequent commands in your session, use export. If you need a variable set for one command only, use env or the shell’s VAR=value command syntax.
When should I use env -i?
Use env -i when you want to verify that a script or program works without relying on inherited environment variables. It is also useful in security-sensitive contexts where you want to prevent a child process from seeing variables like AWS_SECRET_ACCESS_KEY or database credentials that exist in the parent shell.
Why use #!/usr/bin/env bash instead of #!/bin/bash?
The env-based shebang is more portable. On most Linux distributions, bash lives at /bin/bash, but on other Unix systems (FreeBSD, macOS with Homebrew, NixOS) it may be installed elsewhere. Using #!/usr/bin/env bash searches the PATH for the interpreter, so the script works regardless of where bash is installed.
Conclusion
The env command gives you fine-grained control over the environment a process sees without touching your current shell session. For a broader look at how environment and shell variables work in Linux, including persistent configuration and the PATH variable, see the guide on how to set and list environment variables
.
![]()
模拟
解法:模拟
数据范围较小,模拟即可。复杂度 $\mathcal{O}(nq)$。
参考代码(c++)
class Solution {
public:
int xorAfterQueries(vector<int>& nums, vector<vector<int>>& queries) {
int n = nums.size();
long long A[n];
for (int i = 0; i < n; i++) A[i] = nums[i];
const int MOD = 1e9 + 7;
for (auto &qry : queries) for (int i = qry[0]; i <= qry[1]; i += qry[2]) A[i] = A[i] * qry[3] % MOD;
long long ans = 0;
for (int i = 0; i < n; i++) ans ^= A[i];
return ans;
}
};
模拟
非暴力做法(Python/Java/C++/Go)
本题和周赛第四题是一样的,请看 我的题解。
从一个截图函数到一个 npm 包——pdf-snapshot 的诞生记
一个 PDF 文档页面截图工具的渐进式演化之路
背景
事情要从一个内部知识库项目说起。
产品同学提了一个需求:知识库里存了大量 PDF 文档,在预览列表页希望能展示文档的缩略图,用户点击缩略图后再打开完整的 PDF 文件。听起来很简单对吧?但问题是——库里只有 PDF 文件,没有缩略图。
于是摆在我面前的问题就很清晰了:如何从 PDF 文件中生成缩略图?
一番调研后发现,Node.js 生态里虽然有一些 PDF 相关的库,但要么功能太重(整个 PDF 编辑器级别)、要么只能跑在浏览器端、要么 API 设计不太友好。最后决定基于 pdf-parse 封装一个轻量级的截图工具。
本以为写个工具函数就完事了,没想到这个小需求最终演变成了一个完整的 npm 包。下面就来聊聊这个渐进式的演化过程。
渐进式方案演进
阶段一:一个 utils 函数
最初的需求很简单——给知识库用,能生成缩略图就行。
于是我在项目里写了个 utils/pdfSnapshot.ts,核心逻辑大概长这样:
import { PDFParse } from 'pdf-parse';
export async function snapshotPdf(filePath: string, pages: number[]) {
const pdfBuffer = await readFile(filePath);
const pdfParser = new PDFParse({ data: pdfBuffer });
const result = await pdfParser.getScreenshot({
partial: pages,
scale: 1.5,
imageBuffer: true,
});
return result.pages.map(page => ({
page: page.pageNumber,
data: Buffer.from(page.data),
}));
}
嗯,几十行代码,需求搞定,下班!
阶段二:抽成独立模块
好景不长,没过多久,隔壁组的同事找过来了:
"嘿,听说你写了个 PDF 截图的工具?我们这边有个文档预处理服务也需要这个功能,能不能给我们用用?"
于是我把这个函数从业务项目里抽出来,放到了一个独立的内部模块里。
但抽离的过程中发现了一些问题:
-
内存泄漏风险:
pdfjs-dist(pdf-parse的底层依赖)会在内存里缓存解析结果,大量 PDF 处理后内存蹭蹭往上涨 - 缺少取消机制:处理几百页的大文件时,用户等不及想取消,但没有中断的能力
- 输入格式单一:只支持文件路径,不支持 Buffer 和流式输入
既然要给其他模块用了,这些问题就得解决。于是开始了第一次重构:
- 引入子进程隔离,PDF 渲染跑在独立进程里,进程退出后内存自动释放
- 支持 AbortController 取消操作
- 支持文件路径 / Buffer / ReadableStream 三种输入格式
阶段三:发布为 npm 包
又过了一段时间,其他团队的同事也找过来了:
"你们那个 PDF 截图工具挺好用的,我们想在另一个项目里用,能不能发个 npm 包?" "对了,我们有个批量处理的场景,能不能加个进度回调?" "还有,我们运维同学想在脚本里用,能不能支持命令行?"
好家伙,需求越来越多了。
既然要发 npm 包,那就得认真对待了。于是有了这次比较彻底的重构:
- 完善的 TypeScript 类型定义
- 进度回调机制(
onProgress) - CLI 工具支持,方便脚本调用和 AI Agent 集成
- 多种输出格式:Buffer / Base64 / 文件路径
- 超时控制,避免子进程卡死
最终,这个工具从一个几十行的函数,演变成了一个结构完整的 npm 包——@guangmingz/pdf-snapshot。
设计思路与实现框架
聊完演化过程,来深入剖析一下 pdf-snapshot 的设计思路。
核心设计原则
在设计这个工具时,我遵循了几个核心原则:
- 主进程零污染:PDF 渲染是内存大户,不能污染主进程
- 输入输出灵活:支持多种输入格式和输出格式,适应不同场景
- 可控性强:支持取消、超时、进度回调
- API 简洁:一个函数搞定,不需要复杂的初始化流程
模块架构
整个项目的目录结构如下:
src/
├── core/
│ ├── snapshot.ts # 核心截图函数(主进程)
│ ├── pdf-info.ts # 获取 PDF 信息
│ └── worker.ts # 子进程 Worker(实际渲染)
├── utils/
│ ├── input-normalizer.ts # 输入归一化
│ ├── page-resolver.ts # 页码解析
│ ├── output-formatter.ts # 输出格式化
│ └── worker-manager.ts # 子进程管理
├── cli/
│ └── index.ts # 命令行入口
├── types.ts # 类型定义
├── errors.ts # 错误类
├── constants.ts # 常量
└── index.ts # 导出入口
可以看到,模块划分还是比较清晰的:
- core:核心逻辑,包括主进程入口和子进程 Worker
- utils:工具函数,处理输入输出和子进程管理
- cli:命令行接口
子进程隔离:内存泄漏的终极解法
这是整个设计中最关键的一环。
为什么要用子进程?因为 pdfjs-dist 在解析 PDF 时会在 V8 堆上分配大量内存,即使调用了 destroy() 方法,也很难完全释放。如果在主进程里处理大量 PDF,内存会越积越多,最终 OOM。
解法很简单也很粗暴——用子进程。子进程退出后,操作系统会自动回收它占用的所有内存,干净利落。
整个流程如下:
┌─────────────────────────────────────────────────────────────────┐
│ 主进程 (Main Process) │
├─────────────────────────────────────────────────────────────────┤
│ 1. 接收输入 (文件路径 / Buffer / Stream) │
│ 2. 归一化为临时文件路径 │
│ 3. 解析页码参数 │
│ 4. Fork 子进程,传递任务参数 │
│ 5. 等待子进程完成,接收结果文件路径 │
│ 6. 根据 output 参数格式化输出 │
│ 7. 清理临时文件 │
└───────────────────────────┬─────────────────────────────────────┘
│ IPC 通信(传递路径,不传 Buffer)
▼
┌─────────────────────────────────────────────────────────────────┐
│ 子进程 (Worker Process) │
├─────────────────────────────────────────────────────────────────┤
│ 1. 读取 PDF 文件 │
│ 2. 调用 pdf-parse 渲染指定页面 │
│ 3. 将截图写入临时目录 │
│ 4. 返回文件路径 + 元数据 │
│ 5. 退出进程(内存自动释放) │
└─────────────────────────────────────────────────────────────────┘
这里有个细节值得一提:IPC 通信只传文件路径,不传 Buffer。
为什么?因为 IPC 传输大数据很慢,一张截图可能有几 MB,如果通过 IPC 传 Buffer,性能会很差。所以我们让子进程把截图写到临时目录,IPC 只传路径和元数据(宽高、大小),主进程再按需读取。
子进程的核心代码:
process.on('message', async (msg: WorkerRequest) => {
const { pdfPath, pages, scale, outputDir } = msg;
let pdfParser: PDFParse | null = null;
try {
const pdfBuffer = await readFile(pdfPath);
pdfParser = new PDFParse({ data: pdfBuffer });
// 一次性传入所有页码,避免重复解析 PDF
const screenshotResult = await pdfParser.getScreenshot({
partial: pages,
scale,
imageBuffer: true,
});
const results: PageInfo[] = [];
for (const page of screenshotResult.pages) {
const filePath = join(outputDir, `page-${page.pageNumber}.png`);
await writeFile(filePath, Buffer.from(page.data));
results.push({ pageNumber: page.pageNumber, filePath, width: page.width, height: page.height });
}
process.send!({ success: true, pages: results });
} catch (error) {
process.send!({ success: false, error: error.message });
} finally {
await pdfParser?.destroy();
process.exit(0); // 退出进程,内存自动释放
}
});
输入归一化:统一处理多种输入格式
为了支持文件路径、Buffer、ReadableStream 三种输入格式,我设计了一个「输入归一化」层:
export async function normalizeInput(input: PdfInput): Promise<{ path: string; isTempFile: boolean }> {
// 文件路径:直接使用
if (typeof input === 'string') {
return { path: input, isTempFile: false };
}
// Buffer / Stream:写入临时文件
const tempPath = join(tmpdir(), `pdf-${randomUUID()}.pdf`);
if (Buffer.isBuffer(input)) {
await writeFile(tempPath, input);
} else {
// Stream
const chunks: Buffer[] = [];
for await (const chunk of input) {
chunks.push(chunk);
}
await writeFile(tempPath, Buffer.concat(chunks));
}
return { path: tempPath, isTempFile: true };
}
不管用户传什么格式,最终都归一化为文件路径,后续逻辑只需要处理文件路径即可。这种「归一化」的设计模式在很多场景下都很实用。
取消与超时:让操作可控
处理大文件时,用户可能等不及想取消;或者子进程卡死了需要超时兜底。这两个能力是生产环境必备的。
取消能力基于标准的 AbortController:
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // 5 秒后取消
try {
await snapshotPdf('./large.pdf', { signal: controller.signal });
} catch (error) {
if (error instanceof SnapshotAbortedError) {
console.log('操作被取消');
}
}
超时控制在子进程管理器里实现:
const timer = setTimeout(() => {
child.kill('SIGKILL'); // 强制杀死子进程
reject(new SnapshotTimeoutError(timeout));
}, timeout);
进度回调:让等待不再焦虑
批量处理时,用户需要知道当前进度。虽然子进程是一次性处理所有页面的,但我们至少可以在「开始」和「完成」两个时机通知用户:
await snapshotPdf('./document.pdf', {
pageRange: [1, 100],
onProgress: (progress) => {
// progress.stage: 'preparing' | 'completed'
// progress.percent: 0 | 100
console.log(`[${progress.stage}] ${progress.percent}%`);
},
});
为什么不支持逐页进度?因为
pdf-parse的getScreenshot是一次性处理所有页面的,中间没有回调钩子。如果要实现逐页进度,需要改成逐页调用,但这样会有性能问题(每次调用都要重新解析 PDF)。权衡之下,选择了「阶段进度」的方案。
CLI 工具:让 AI 也能用
最后聊聊 CLI 工具。
为什么要做 CLI?除了方便运维同学写脚本,还有一个重要原因——方便 AI Agent 调用。
现在各种 AI 编程助手越来越流行,它们通常通过命令行来调用工具。如果你的工具只有 API 没有 CLI,AI 就很难直接使用。
pdf-snapshot 的 CLI 使用起来很简单:
# 截取第 1-10 页
pdf-snapshot -r 1-10 -o ./output document.pdf
# 截取指定页
pdf-snapshot -p 1,5,10 document.pdf
# 从标准输入读取(支持管道)
cat document.pdf | pdf-snapshot -o ./output -r 1-5 -
# 仅查看 PDF 信息
pdf-snapshot --info document.pdf
CLI 的实现基于 commander,核心是把命令行参数映射到 snapshotPdf 的 options:
program
.argument('<input>', 'PDF 文件路径')
.option('-o, --output <dir>', '输出目录', './pdf-screenshots')
.option('-p, --pages <pages>', '离散页码')
.option('-r, --range <range>', '页码范围')
.option('-s, --scale <number>', '缩放比例', '1.5')
.action(async (input, opts) => {
const results = await snapshotPdf(input, {
output: 'file',
outputDir: opts.output,
pageRange: parseRange(opts.range),
pages: parsePages(opts.pages),
scale: parseFloat(opts.scale),
});
console.log(`✅ 完成!已保存 ${results.length} 张截图`);
});
还贴心地加了进度条:
⏳ 正在截图...
[████████████████████████████████████████] 100% | 50/50 页
✅ 完成!已保存 50 张截图到 ./pdf-screenshots
总结
回顾 pdf-snapshot 的演化过程:
- 阶段一:一个 utils 函数,解决单点需求
- 阶段二:抽成独立模块,解决内存泄漏、支持取消和多种输入格式
- 阶段三:发布 npm 包,增加进度回调、CLI 工具、完善类型定义
这个过程其实挺有代表性的。很多时候我们写的工具函数,一开始只是为了解决眼前的问题,但随着需求的增加和使用场景的扩展,它会逐渐演化成一个更通用、更健壮的模块。
关键是要在演化过程中保持代码的可维护性和可扩展性。子进程隔离、输入归一化、取消超时机制……这些设计不是一开始就有的,而是在实际使用中逐步发现问题、解决问题后沉淀下来的。
最后,如果你也有 PDF 截图的需求,欢迎试试 pdf-snapshot!
GitHub 地址:pdf-snapshot
有问题欢迎提 Issue,有改进想法欢迎 PR!
字节/腾讯内部流出!Claude Code 2026王炸玩法!效率暴涨10倍
还在把 Claude 当“高级代码抄写员”?
让它写个函数、改个 bug,一问一答像聊天?
大错特错! 2026 年的 Claude Code 早已进化成自主 AI 开发智能体——
它能自己读项目、自己规划、自己写代码、自己跑测试、自己修 bug,甚至直接操控你的电脑完成全流程开发!
真实案例:
字节某团队用 Claude Code 的 Subagents(多智能体) 功能,30 分钟交付一个带用户认证的完整博客系统;
腾讯某工程师靠 Computer Use(电脑直控),让 AI 自动部署项目、复现并修复 UI Bug,全程无需动手。
今天这篇,把 Claude Code 2026 最强玩法、最新功能、隐藏技巧、实战避坑 一次性讲透,看完直接从新手变大神!
一、先看效果:以前累死,现在躺赢
| 场景 | 旧方式 | Claude Code 新方式 |
|---|---|---|
| 开新对话 | 重复解释项目架构、规范 | Kairos 长期记忆自动加载上下文 |
| 部署项目 | 手动敲命令、点 Vercel | Computer Use 自动操作 GUI 完成 |
| 复杂开发 | 一人单干,耗时一天 | Subagents 派出 AI 团队并行开发 |
| 关机后 | 任务中断 | /schedule 云端继续跑 |
核心价值:
从“人写代码” → “人定目标,AI 自主完成全流程”
二、2026 三大王炸功能(官方 3 月刚上线)
1. Computer Use:AI 直接操控你的 macOS 电脑
这是 AI 编程的革命性突破!
Claude 不再局限于代码文本,而是像人一样操作系统:
- ✅ 自动打开终端、执行
npm install - ✅ 截图识别报错弹窗、日志
- ✅ 点击按钮、填写表单、操作 GUI 工具
- ✅ 完整 Debug 循环:运行→报错→修改→再运行
实战场景:
“帮我部署这个 React 项目到 Vercel”
→ Claude 自动登录 Vercel → 构建 → 部署 → 返回结果
全程你只需要看着!
注意:目前仅支持 macOS + Pro/Max 订阅,需授权安全目录。
2. Subagents:召唤你的 AI 开发团队
一个 Claude 不够用?直接派多个分身并行工作!
- 前端组:开发页面、写样式
- 后端组:设计 API、写逻辑
- 测试组:编写用例、跑测试
- 安全组:审查漏洞、提建议
效率提升:日常开发 3-5 倍,复杂项目 10 倍+!
3. Kairos 长期记忆:AI 永远记住你的项目
解决“金鱼记忆”痛点——跨会话永久记忆 + 自动整理。
启用方式:
在项目根目录创建 CLAUDE.md,Claude 自动读取并永久记忆。
# 项目规范(示例)
- 技术栈:React 18 + TypeScript
- 代码规范:ESLint + Prettier
- 命名:小驼峰,组件名大写开头
- 禁止:直接修改 src/legacy 目录
下次打开,无需重复解释任何信息!
三、硬核对比:为什么 Claude Code 是 2026 最强?
SWE-bench 权威数据(复杂任务通过率)
- Claude Opus 4.6:80.8%(行业第一)
- GPT-5.2:80.0%
- Cursor(GPT-5 后端):61.3%
Token 效率:省 5.5 倍成本
同样复杂任务:
- Claude Code:33,000 tokens,零错误
- Cursor:188,000 tokens,多次报错
适用场景对比
| 工具 | 最佳场景 | 劣势 |
|---|---|---|
| Claude Code | 大型项目、全流程开发、跨文件重构 | 界面极简,学习曲线略陡 |
| Cursor | 前端快速开发、实时补全 | 复杂项目理解弱 |
| Copilot | 单行补全、IDE 集成 | 自主能力差 |
结论:
做正经开发,选 Claude Code;简单业务,选 Cursor。
四、90% 人不知道的隐藏技巧
1. 7 个必学斜杠命令
/auto # 全自动模式,AI 自主决策
/debug # 查看会话状态、工具调用
/skill list # 查看所有可用技能
/schedule # 云端定时任务(关机后继续跑)
/context clear # 清理上下文,防“变笨”
/llm # 切换模型(Sonnet/Opus)
2. 提示词黄金公式
角色+目标+规范+示例+约束
【角色】资深全栈,精通 React+TS
【目标】开发登录页,含表单验证
【规范】Tailwind CSS,小驼峰命名
【示例】参考注册页风格
【约束】响应式,支持移动端
3. Computer Use 安全玩法
- 开启
Safe Mode:敏感操作需二次确认 - 用
/allow dir ./my-project限定工作目录 - 重要操作前手动审查 AI 计划
五、新手 3 步速成指南(闭眼操作)
第 1 步:安装+配置
npm install -g @anthropic/claude-code
claude login
claude config set model opus-4.6
claude config set computer_use true
第 2 步:必装 Skills 包
# 添加市场
claude market add official
claude market add https://github.com/affaan-m/everything-claude
# 安装神级包
claude install everything-claude # 60+ 全能技能
claude install kairos-mem # 长期记忆
claude install computer-use-pro # 电脑直控增强
第 3 步:最佳工作流
- 项目根目录创建
CLAUDE.md - 启动 Claude:
claude - 启用技能:
/skill use everything-claude - 下达目标:“帮我分析项目,规划开发计划”
- 确认后执行:
/auto
六、避坑指南:7 个常见错误
| 错误 | 正确做法 |
|---|---|
| 把 Claude 当聊天机器人 | 给完整角色、目标、规范 |
| 不设权限,放任 AI 操作 | 严格限定目录,开安全模式 |
| 上下文爆炸不清理 | 定期 /context clear
|
| 所有任务用最贵模型 | 简单用 Sonnet,复杂用 Opus |
| 忽略 Computer Use 安全 | 仅授权工作目录,手动审查 |
七、2026 选型指南:谁最适合用?
必选 Claude Code,如果你是:
- 后端/全栈,做复杂业务系统
- 架构师,负责大型项目重构
- 技术团队,追求效率最大化
- 独立开发者,想一个人顶一个团队
考虑其他工具,如果你是:
- 纯前端,只做快速页面(选 Cursor)
- 学生/新手,追求简单易用(选 Cursor)
- 仅需单行补全(选 Copilot)
结语:AI 编程已进入 2.0 时代
2026 年,AI 编程不再是“辅助”,而是“主力”。
Claude Code 代表的自主智能体开发模式,正在彻底重构软件开发流程。
今天学会 Claude Code,不是掌握一个工具,而是抢占 AI 时代的开发效率制高点。
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!
大厂都在偷偷用的 Cursor Rules 封装!告别重复 Prompt,AI 编程效率翻倍
还在每天教 AI “失忆实习生”?
每次开新对话都要重讲项目架构、代码规范、命名风格……
而用这套 Rules 配置,Cursor 直接变成“懂你心思的老搭档”——跨会话记忆、自动守规范、团队统一标准,字节/腾讯内部已全面落地!
如果你受够了:
- 写 200 行 Prompt 只为让 AI 记住项目
- 团队成员各自为战,AI 输出风格五花八门
- 代码不规范、漏 error、命名混乱,返工到崩溃
那么,这篇经过 GitHub 5w+ 星验证的 Rules 完全指南,就是为你写的——
不用背命令,装完就忘,自然语言写需求,AI 自动按规矩干活!
一、先看效果:以前累死,现在躺赢
| 场景 | 旧方式 | Rules 新方式 |
|---|---|---|
| 开新对话 | 粘贴 300 行项目背景 | 自动加载上下文 |
| 写 Go 代码 | 手动提醒“加注释、小驼峰” | 自动遵守官方规范 |
| 团队协作 | 每人一套 Prompt,风格乱飞 | 共享规则包,输出统一 |
| 代码审查 | 人工查漏 | 自动 lint + 安全扫描 |
真实收益:
- 每天节省 1~2 小时重复解释
- 代码返工率下降 70%
- 新人上手 AI 编程速度提升 3 倍
二、Rules 是什么?
传统 Prompt = 便利贴
贴一次用一次,新开对话就丢。
Rules = 永久工作手册
写一次,全局生效,团队共享,Git 可管。
核心价值:
让 Cursor 从“聪明但没规矩” → “专业且守纪律”
三、必装三大神级 Rules(附一键安装)
1. everything-cursor(闭眼装)
GitHub ⭐ 52k+,黑客松冠军,60+ 规则覆盖全开发流程。
支持:Go / Java / Python / React / Rust / Next.js
能力:自动格式化、TDD、部署提示、安全检查
命令:/fmt /check /tdd /mem
/rule market add https://github.com/affaan-m/everything-cursor
/rule install everything-cursor@everything-cursor
2. cursor-mem(解决金鱼记忆)
GitHub ⭐ 23k+,跨会话永久记住你的项目!
再也不用重复解释:
- 业务逻辑
- 技术架构
- 特殊约束
/rule market add https://github.com/thedotmack/cursor-mem
/rule install cursor-mem@cursor-mem
3. super-rules(工程化最强)
GitHub ⭐ 28k+,让 AI 像资深工程师一样思考。
TDD 测试驱动
结构化调试
代码审查分级(Minor/Normal/Critical)
/rule market add https://github.com/obra/cursor-super-rules.git
/rule install super-rules@cursor-super-rules
避坑:装了
everything-cursor就别装plan-with-code,指令冲突!
四、Rules 怎么工作?完全无感!
99% 的规则安装后自动生效,无需手动触发!
Cursor 会智能判断:
- 你打开的是 Go 文件 → 自动激活
go-standard - 你在写前端 → 自动加载
react-rules - 你问架构问题 → 启用
arch-rules
你只需要用自然语言描述需求,剩下的交给 Rules!
五、两种安装方式(推荐第一种)
方式一:市场导入(推荐)
# 添加市场(只需一次)
/rule market add https://github.com/getcursor/cursor-rules-official
# 安装规则包
/rule install go-pack@cursor-rules-official
方式二:本地手动(私有/离线场景)
# macOS/Linux
cp -r my-rule ~/.cursor/rules/
六、新手安装优先级(直接抄作业)
| 优先级 | 规则包 | 理由 |
|---|---|---|
| 第一 | everything-cursor | 全能王,闭眼装 |
| 第二 | cursor-mem | 解决记忆痛点 |
| 第三 | super-rules | 提升工程质量 |
| 4 | code-fmt(官方) | 自动格式化 |
| 5 | rule-maker(官方) | 自定义规则 |
七、大厂为什么都在用?
-
字节:用
cursor-mem统一 200+ 微服务上下文 -
腾讯:
super-rules接入 CI,代码审查自动化 -
阿里云:基于
rule-maker生成内部规范包
Rules 不是玩具,而是 AI 编程的“基础设施”
结语:AI 编程,进入“有纪律”时代
当你不再为重复解释焦头烂额,
当你团队的 AI 输出风格高度统一,
你就知道——Rules,是每个专业开发者必须掌握的生产力核弹。
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!
RainbowKit快速集成多链钱包连接:从“连不上”到丝滑切换的踩坑实录
背景
上个月,我接手了一个新的DeFi聚合器项目的前端重构。这个项目的老前端用的是web3modal + 自定义的链配置,代码已经有点“祖传”的味道了,每次加一条新链都得手动改好几个配置文件,测试起来也麻烦。产品经理提了新需求:要快速支持Arbitrum、Optimism、Polygon等七八条EVM链,并且用户切换链的体验要足够丝滑。
我评估了一下,自己从头用wagmi去搭一套连接组件,虽然灵活,但时间成本太高,光是设计UI和处理好各种边缘情况(比如用户钱包里没添加该链)就得花上好几天。这时候,我想到了RainbowKit——一个基于wagmi构建的、开箱即用的钱包连接套件,UI漂亮,文档说支持多链配置。心想,用它应该能快速搞定,把时间省下来去处理更复杂的业务逻辑。于是,我的“快速集成”之旅开始了,没想到,快是快了,坑也是一个没少踩。
问题分析
一开始,我的思路很简单:照着RainbowKit官方文档的“Getting Started”部分,安装依赖,用getDefaultConfig搞个配置,把RainbowKitProvider和WagmiProvider一套,最后把ConnectButton一扔,不就完事了吗?我最初的核心配置代码是这样的:
import { getDefaultConfig, RainbowKitProvider } from '@rainbow-me/rainbowkit';
import { WagmiProvider } from 'wagmi';
import { mainnet, polygon, optimism, arbitrum } from 'wagmi/chains';
const config = getDefaultConfig({
appName: 'My DeFi App',
projectId: 'YOUR_PROJECT_ID', // 从WalletConnect Cloud拿的
chains: [mainnet, polygon, optimism, arbitrum],
});
function App() {
return (
<WagmiProvider config={config}>
<RainbowKitProvider>
<ConnectButton />
</RainbowKitProvider>
</WagmiProvider>
);
}
跑起来一看,连接MetaMask确实没问题,主网也能用。但当我尝试切换到Polygon时,问题来了。点击切换,钱包弹窗倒是出来了,但要么是提示“未添加网络”,要么是切换后前端的链ID显示还是1(以太坊主网)。控制台里时不时飘过一些关于RPC URL的警告。
我意识到,问题出在链的配置上。getDefaultConfig和从wagmi/chains导入的链定义,其RPC端点可能是公共的,有速率限制或不稳定。而且,对于用户钱包里没有的链,RainbowKit的默认行为可能和我想的不一样。我需要更精细地控制每条链的配置,特别是RPC,并且要处理好钱包添加网络的流程。这不是一个“五分钟集成”就能完事的问题,需要深入配置。
核心实现
第一步:自定义链配置,搞定稳定的RPC
公共RPC是第一个坑。尤其是在测试网或者Polygon这类链上,公共RPC经常不稳定,导致交易发送失败或者读取数据超时。我的解决方案是使用项目自己的Infura或Alchemy节点,如果没有,也可以选择一些更可靠的公共服务商如publicnode.com。
这里有个关键点:RainbowKit(或者说底层的wagmi v2)的链配置对象,需要包含rpcUrls字段,并且要正确区分default和public。我一开始没注意,直接覆盖错了,导致钱包连接内部调用还是走了不稳定的节点。
// chains/customChains.ts
import { Chain } from 'wagmi/chains';
// 自定义Polygon链配置
export const customPolygon: Chain = {
id: 137,
name: 'Polygon',
network: 'matic',
nativeCurrency: {
name: 'MATIC',
symbol: 'MATIC',
decimals: 18,
},
rpcUrls: {
// default 和 public 最好都配置,default用于钱包写操作,public用于前端读操作
default: {
http: ['https://polygon-mainnet.g.alchemy.com/v2/YOUR_API_KEY'], // 你的Alchemy或Infura URL
},
public: {
http: ['https://polygon-rpc.com'], // 一个可靠的公共RPC
},
},
blockExplorers: {
default: { name: 'PolygonScan', url: 'https://polygonscan.com' },
},
contracts: {
multicall3: {
address: '0xca11bde05977b3631167028862be2a173976ca11',
blockCreated: 25770160,
},
},
};
// 同理,配置其他链,比如Arbitrum
export const customArbitrum: Chain = {
id: 42161,
name: 'Arbitrum One',
network: 'arbitrum',
nativeCurrency: {
name: 'Ether',
symbol: 'ETH',
decimals: 18,
},
rpcUrls: {
default: {
http: ['https://arb1.arbitrum.io/rpc'],
},
public: {
http: ['https://arb1.arbitrum.io/rpc'],
},
},
blockExplorers: {
default: { name: 'Arbiscan', url: 'https://arbiscan.io' },
},
contracts: {
multicall3: {
address: '0xca11bde05977b3631167028862be2a173976ca11',
blockCreated: 7654707,
},
},
};
第二步:配置RainbowKit与Wagmi
有了自定义的链配置,接下来就是正确创建wagmi的config对象。这里我放弃了getDefaultConfig这个快捷方法,因为它对配置的控制不够细。我改用createConfig手动配置,这样可以明确指定传输层(transport)和连接器。
注意这个细节:wagmi的createConfig需要为每条链单独创建transport。我在这里又踩了个坑,试图用一个transport给所有链用,结果只有主网能正常工作。
// config/wagmiConfig.ts
import { http, createConfig } from 'wagmi';
import { mainnet } from 'wagmi/chains';
import { customPolygon, customArbitrum, customOptimism } from '../chains/customChains';
import { getDefaultWallets } from '@rainbow-me/rainbowkit';
// 定义项目支持的链数组
const projectChains = [mainnet, customPolygon, customArbitrum, customOptimism] as const;
// 1. 设置钱包连接器 (RainbowKit提供)
const { connectors } = getDefaultWallets({
appName: 'My DeFi Aggregator',
projectId: 'YOUR_WALLETCONNECT_PROJECT_ID', // 必须去WalletConnect Cloud创建项目获取
chains: projectChains,
});
// 2. 创建Wagmi配置
export const config = createConfig({
chains: projectChains,
transports: {
// 为每条链分别创建transport,使用我们自定义的RPC
[mainnet.id]: http(mainnet.rpcUrls.default.http[0]), // 也可以用你的主网节点
[customPolygon.id]: http(customPolygon.rpcUrls.default.http[0]),
[customArbitrum.id]: http(customArbitrum.rpcUrls.default.http[0]),
[customOptimism.id]: http(customOptimism.rpcUrls.default.http[0]),
},
connectors, // 注入RainbowKit生成的连接器
ssr: false, // 如果不是Next.js等SSR框架,可以设为false
});
第三步:集成到React应用中并实现链切换
配置完成后,在应用根组件中注入Provider就相对简单了。但为了让用户能方便地切换链,我不仅使用了ConnectButton(它自带切换网络的下拉菜单),还在应用内部关键位置(比如资产面板顶部)添加了一个手动的链切换器,使用useSwitchChain这个hook。
这里有个用户体验上的坑:如果用户的钱包里没有添加你指定的链,直接调用switchChain会失败。RainbowKit的ConnectButton下拉菜单会自动处理这个情况(触发钱包添加网络),但自己写的切换器需要手动处理。我的做法是捕获错误,然后调用addChain。
// components/ChainSwitcher.tsx
import { useChainId, useSwitchChain, useChains } from 'wagmi';
import { useCallback } from 'react';
export function ChainSwitcher() {
const currentChainId = useChainId();
const { switchChain } = useSwitchChain();
const supportedChains = useChains();
const handleSwitch = useCallback(async (targetChainId: number) => {
if (targetChainId === currentChainId) return;
try {
await switchChain({ chainId: targetChainId });
} catch (error: any) {
// 错误码 4902 是钱包(如MetaMask)提示用户添加网络的标准错误
if (error?.code === 4902) {
// 在实际项目中,这里应该弹出一个更友好的提示,引导用户去ConnectButton那里切换,或者手动触发addChain。
// 因为addChain API需要完整的链信息,直接从supportedChains里找。
const targetChain = supportedChains.find(c => c.id === targetChainId);
if (targetChain) {
console.warn(`请手动在钱包中添加 ${targetChain.name} 网络,或使用右上角的连接按钮进行切换。`);
// 可以在这里调用 window.ethereum.request({ method: 'wallet_addEthereumChain', params: [targetChainInfo] })
}
}
console.error('切换链失败:', error);
}
}, [currentChainId, switchChain, supportedChains]);
return (
<div className="chain-switcher">
<span>当前网络: </span>
<select
value={currentChainId}
onChange={(e) => handleSwitch(Number(e.target.value))}
>
{supportedChains.map((chain) => (
<option key={chain.id} value={chain.id}>
{chain.name}
</option>
))}
</select>
</div>
);
}
完整代码示例
下面是一个简化但可运行的应用根组件示例,整合了上述所有配置:
// App.tsx
import { WagmiProvider } from 'wagmi';
import { RainbowKitProvider, darkTheme, ConnectButton } from '@rainbow-me/rainbowkit';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from './config/wagmiConfig';
import { ChainSwitcher } from './components/ChainSwitcher';
import '@rainbow-me/rainbowkit/styles.css'; // 不要忘记引入样式!
// 为Wagmi的缓存创建QueryClient
const queryClient = new QueryClient();
function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider
theme={darkTheme()} // 可以自定义主题
coolMode // 开启酷炫的按钮效果
locale="en-US" // 设置语言
>
<div className="app">
<header>
<h1>我的DeFi聚合器</h1>
<div className="wallet-section">
<ConnectButton
accountStatus="full" // 显示完整地址
chainStatus="icon" // 只显示链图标,不显示名称
showBalance={false}
/>
</div>
</header>
<main>
<div className="network-panel">
<ChainSwitcher />
</div>
{/* 你的其他业务组件 */}
<div>业务内容区域...</div>
</main>
</div>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
export default App;
踩坑记录
-
projectId无效或缺失导致的静默失败:最开始我没仔细看文档,随便写了个字符串当projectId。结果钱包连接(尤其是WalletConnect)时,移动端扫码后一直连接不上,前端也没明显报错。解决方法:必须去WalletConnect Cloud创建项目,获取真实的projectId。 -
链切换后,前端状态不同步:点击切换链,钱包成功了,但应用里
useChainId()返回的还是旧的链ID。排查发现:这是因为我在不同的地方用了不同的wagmi配置实例,或者Provider包裹层级有问题。解决方法:确保整个应用只用一个config,且WagmiProvider包裹了所有用到wagmi hook的组件。 -
自定义链的图标不显示:RainbowKit为一些主流链内置了图标,但自定义链或一些较新的链(比如Base)可能没有。解决方法:可以通过
RainbowKitProvider的chainImages属性来注入自定义链图标,是一个{ [chainId: number]: string }的映射,值为图片URL。 -
SSR(Next.js)下的水合错误:在Next.js项目里,因为服务端和客户端初始状态可能不一致(比如连接的钱包信息),会导致水合错误。解决方法:RainbowKit提供了
SSRProvider组件来配合Next.js的App Router使用。同时,将wagmi配置中的ssr设为true,并确保连接状态相关的UI在客户端渲染后再显示(用useEffect或useState控制)。
小结
这次集成让我体会到,RainbowKit确实能极大加速Web3应用钱包连接部分的开发,但它不是“无脑”配置就能应对所有生产环境需求的。核心收获是:多链支持的关键在于稳定且可控制的RPC配置,以及对“用户钱包可能未添加链”这一情况的妥善处理。 下一步,可以继续深挖RainbowKit的主题定制、与Zustand/Redux的状态集成,以及如何优雅地处理连接断开和重连的逻辑。
AI 全流程解析(LLM / Token / Context / RAG / Prompt / Tool / Skill / Agent)
前言
AI圈子里每天都在冒新名词:LLM、Token、Context、Prompt……这些词你可能都听说过。但是,你真的能准确说出其中每一个概念的确切含义吗?
这篇文章不整那些虚头巴脑的商业概念,我们从最底层的工程视角出发,一个一个把这些概念拆开、揉碎讲清楚。相信读完这篇文章,你对AI的理解绝对会上升一个台阶。
![]()
一、LLM(大语言模型)
1.1 什么是LLM?
LLM全称是Large Language Model,翻译成中文就是大语言模型,简称大模型。
基本上现在所有的大模型都是基于Transformer这套架构训练出来的。这个架构最早由Google团队在2017年提出,对应的论文名是《Attention is All You Need》。
极具戏剧性的是,虽然Google发明了火种,但真正把它点燃并且引爆全世界的却是OpenAI。
1.2 大模型的发展历程
- 2020年:GPT-3发布,已经具备初步实用价值
- 2022年底:GPT-3.5横空出世,真正让大模型进入大众可用阶段
- 2023年3月:GPT-4发布,把AI的能力天花板拉到了新高度
- 至今:Claude等优秀后起之秀在各领域与OpenAI同台竞技
GPT系列是今天AI浪潮的绝对鼻祖,时至今日,GPT家族依然非常强大,GPT-4仍是业界标杆之一。
1.3 大模型的工作原理
大模型到底是怎么工作的?答案非常简单朴素:它本质上就是一个文字接龙游戏。
虽然本质是"预测下一个Token",但通过大规模训练,这种能力涌现出了推理、总结、代码生成等复杂行为。
具体例子:
假设你向大模型提问:"这个产品怎么样?"
- 模型接收这句话后,经过内部运算,预测下一个概率最高的词:"非常"
- 模型把"非常"抓回来,追加到输入后面
- 继续预测下一个字:"好"
- 再把"好"塞回去,继续预测:"用"
- 最后输出结束标识符
完整回答:"非常好用"
这就是为什么大模型要一个词一个词地输出答案——因为它就是这么运作的。
一句话总结:大模型 = 一个极其复杂的"概率文字接龙机器"
二、Token(词元)
2.1 文字与数字的桥梁
大模型本质上是一个庞大的数学函数,里面跑的全是矩阵运算。它接收的是数字,输出的也是数字,压根儿就不认识人类写的文字。
在人类和大模型之间必须有一个中间人来做翻译,这个中间人就叫做Tokenizer(分词器),负责编码和解码两件事情:
- 编码:把文字变成数字
- 解码:把数字还原成文字
2.2 Token的生成过程
以用户提问"这个产品怎么样?"为例,这句话通常会被切分成若干个Token(具体数量取决于模型的分词策略,不同模型可能略有差异)。
第一步:切分
把用户的问题拆成一个一个最小的片段,这些片段就叫做Token。
第二步:映射
把每个Token对应到一个数字上去,这个数字就叫做Token ID。
2.3 Token vs 词:不是一对一关系
你可能会想:Token就是词,对吧?
不一定! Token和词并没有明确的一对一关系。
中文例子:
- "工作坊" → 拆成"工作"+"坊"(2个Token)
- "程序员" → 拆成"程序"+"员"(2个Token)
英文例子:
- "hello" → 1个Token
- "going" → 1个Token
- "helpful" → 拆成"help"+"ful"(2个Token)
特殊字符:
某些情况下,一个字符会被切分成多个Token。比如"✓"(对勾)需要3个Token来表示。
2.4 Token的估算标准
平均来讲:
- 1个Token ≈ 0.75个英文单词
- 1个Token ≈ 1.5~2个汉字
举例:
- 40万个Token ≈ 60~80万个汉字
- 40万个Token ≈ 30万个英文单词
总结:Token是模型自己学会的一套文本切分规则,切出来的每一块就是它一次能够处理的最小单位。
一句话总结:Token是模型理解世界的"最小语言单位"
三、Context(上下文)
3.1 大模型的"记忆"之谜
我们平时和大模型聊天,它好像能记住之前说的话。比如你开头告诉他"我的名字是小明",他给你回复以后,你再问他"我叫什么名字",他还是能够回答得出来。
但问题是,大模型本质上只是一个数学函数,它并不像人一样真的有记忆。它是怎么记住之前的聊天内容的呢?
答案:每次给大模型发送消息时,并不只会发我们的问题,背后的程序会自动把之前的整段对话历史找出来,一起发过去。
3.2 什么是Context?
Context(上下文)代表大模型每次处理任务时所接触到的信息总和。
Context包含的内容:
- 用户问题
- 对话历史
- 大模型正在输出的每个Token
- 工具列表
- System Prompt(系统提示词)
- 其他信息
可以把Context看成是大模型的一个临时记忆体。
3.3 Context Window(上下文窗口)
Context Window代表Context能够容纳的最大Token数量。
主流模型的Context Window:
| 模型 | Context Window |
|---|---|
| GPT-4 | 128K(约105万Token) |
| Claude 3.1 Pro | 100万Token |
| Claude Opus 4 | 100万Token |
100万个Token ≈ 150万个汉字,整个《哈利波特》全集的内容都能装下。
技术细节:为什么Context会影响推理能力?因为大模型进行复杂推理(如链式思考)需要足够长的上下文来"记住"推理过程。没有足够的Context,模型无法进行多步骤的逻辑推演。
一句话总结:Context不是记忆,是一次性打包输入
四、RAG(检索增强生成)
4.1 问题场景
假设你有一个上千页的公司产品手册,你希望大模型根据这个手册来回答用户的各种疑问,要怎么实现?
4.2 不好的方案
把手册的全部内容跟着用户问题一起扔给大模型?
问题:
- 产品手册太长
- 即使模型的Context Window不被撑爆,成本也无法控制
4.3 RAG解决方案
RAG(Retrieval-Augmented Generation,检索增强生成)可以从产品手册中抽取与用户问题最为匹配的几个片段,然后只把这几个片段发给大模型。
优势:
- 不受Context Window大小限制
- 成本大大降低
- 大模型接收的不是一整本书,可能只是几段话
RAG vs Fine-tuning:RAG是检索时动态注入知识,适合知识频繁更新的场景;微调是将知识嵌入模型参数,适合特定任务的优化。两者可以结合使用。
一句话总结:RAG = 给大模型外挂一个可检索的知识库
五、Prompt(提示词)
5.1 什么是Prompt?
Prompt(提示词)是大模型接收的具体问题或指令。
比如你向大模型提需求:"帮我写一首诗。"这句话就是Prompt。
不要把Prompt想成特别复杂高端的东西,它只不过就是给大模型的一个问题或者是指令而已。
5.2 为什么Prompt很重要?
如果你只是简单地说"帮我写一首诗",大模型可能会:
- 写古诗
- 写现代诗
- 写打油诗
原因:Prompt太模糊,它不知道你具体想要什么。
5.3 如何写好Prompt?
一个好的Prompt应该是:清晰的、具体的、明确的。
好的例子:
"请帮我写一首五言绝句,主题是秋天的落叶,风格要悲凉一点。"
这样一来,大模型就清楚多了,生成的内容也更符合你的预期。
5.4 Prompt Engineering(提示词工程)
Prompt Engineering本质上不是"黑科技",而是"把话说清楚"——这也是为什么它门槛并不高。
现状:
- 门槛较低,本质上就是把话说清楚
- 大模型能力越来越强,即使提示词含糊不清,大模型也能大致猜出你的意图
- 现在还在提它的人寥寥无几
一句话总结:Prompt = 你和大模型沟通的唯一接口
六、User Prompt vs System Prompt
6.1 两种不同的Prompt
有些时候我们不仅要告诉大模型它要处理的具体任务,还要告诉它人设和做事规则。
User Prompt(用户提示词)
- 定义:说明具体任务的Prompt
- 来源:用户自己在对话框输入
- 示例:"3加5等于几?"
System Prompt(系统提示词)
- 定义:说明人设和做事规则的Prompt
- 来源:开发者在后台配置
- 示例:"你是一个耐心的数学老师,当学生问你数学问题的时候,不要直接给出答案,而是要一步一步引导学生思考,帮助他们理解解题思路。"
6.2 具体例子
场景:做一个数学辅导机器人
System Prompt(后台设置,用户看不到):
"你是一个耐心的数学老师,当学生问你数学问题的时候,不要直接给出答案,而是要一步一步引导学生思考,帮助他们理解解题思路。"
User Prompt(学生输入):
"3加5等于几?"
大模型的回答:
"我们可以这样想,你手里有三个苹果,然后又拿了5个,现在一共有多少个呢?你可以数一数看。"
对比:如果没有System Prompt,大模型可能直接说"8"了。
七、Tool(工具)
![]()
7.1 大模型的弱点
大模型有一个明显的弱点:无法感知外界环境。
没有Tool的大模型,本质上是"闭着眼睛说话"。
例子:
假设你问大模型:"今天上海的天气怎么样?"
它可能会说:
"抱歉,我无法获取实时天气信息。我的知识库截止到某年某月,无法提供当前的天气数据。"
原因:大模型只是个文字接龙游戏,它的能力是根据训练数据来预测下一个词,但它真的没有办法去查天气预报网站拿到实时的天气数据。
7.2 什么是Tool?
Tool(工具)本质上就是一个函数,你给它输入,它就给你输出。
天气查询工具例子:
- 输入:城市、日期(两个参数)
- 内部操作:调用气象局的接口
- 输出:天气信息
有了工具,大模型就可以回答天气相关的问题了。
7.3 工作流程
完整流程涉及的角色:
- 用户:提出问题
- 大模型:理解问题,决定是否需要调用工具
- 工具:执行具体任务,返回结果
- 大模型:根据工具返回的结果,生成最终回答
具体步骤:
- 用户问:"今天上海天气怎么样?"
- 大模型识别到需要天气信息
- 大模型调用天气查询工具,传入参数:城市="上海",日期="今天"
- 工具调用气象局API,返回天气数据
- 大模型根据天气数据生成自然语言回答
- 用户收到:"今天上海晴,气温25°C,适合出行。"
7.4 常见工具类型
| 工具类型 | 功能 | 示例 |
|---|---|---|
| 搜索工具 | 实时搜索互联网信息 | Google搜索、Bing搜索 |
| 计算工具 | 执行数学计算 | Python代码执行器 |
| 数据库工具 | 查询数据库 | SQL查询工具 |
| API工具 | 调用外部服务 | 天气API、股票API |
| 文件工具 | 读写文件 | 文档处理工具 |
一句话总结:Tool = 让大模型睁开眼睛看世界的能力
八、Skill(技能)
我帮你写一版“风格统一的”👇(可以直接用)
什么是Skill?
Skill(技能)是针对特定任务的预配置能力包。它把大模型、Prompt、工具、记忆等组件打包在一起,形成一个可以直接使用的功能模块。
如果说:
- Tool 是一个个“工具函数”
- 那 Skill 就是把多个 Tool + Prompt + 执行流程 组合在一起,形成一个可复用的能力模块
Skill的组成
| 组件 | 说明 | 示例 |
|---|---|---|
| 大模型配置 | 选择哪个模型 | GPT-4、Claude、Gemini |
| System Prompt | 人设和任务规则 | "你是一个Python代码专家" |
| 工具列表 | 可调用的工具 | 代码执行器、Git操作 |
| 记忆配置 | 是否需要记忆 | 短期记忆、长期记忆 |
| 输出格式 | 结果的格式要求 | JSON、Markdown、代码块 |
Skill vs Agent vs SubAgent
| 对比维度 | Skill | Agent | SubAgent |
|---|---|---|---|
| 定位 | 功能模块 | 任务执行者 | 辅助执行者 |
| 配置 | 预配置 | 动态配置 | 由Agent分配 |
| 复用性 | 高(可跨项目) | 中(项目内) | 低(任务内) |
| 自主性 | 低(被动调用) | 高(自主规划) | 低(被动执行) |
Skill的生态系统
开源Skill库:
- GitHub上的Skill仓库
- 社区贡献的Skill包
- 可直接下载使用
商业Skill市场:
- 官方Skill商店
- 第三方Skill平台
- 付费/免费Skill
自定义Skill:
- 根据业务需求定制
- 企业内部Skill库
- 持续迭代优化
九、Agent(智能体)
9.1 从大模型到Agent
前面我们学习了Tool,让大模型能够调用外部函数来获取信息。但这里有个问题:谁来决定什么时候调用工具?调用哪个工具?工具返回的结果怎么处理?
这就是Agent(智能体)登场的时候了。
Agent是大模型的进阶形态,它不仅能理解用户需求,还能自主规划和执行任务。
Agent不是更聪明,是更会做事。
9.2 Agent的核心能力
一个完整的Agent通常具备以下能力:
1. 感知能力
理解用户的意图和当前环境信息。
2. 规划能力
把复杂任务拆解成多个步骤,制定执行计划。
3. 执行能力
调用工具、访问外部API、操作文件等。
4. 反思能力
评估执行结果,调整策略,重新规划。
9.3 Agent vs 大模型的区别
| 对比维度 | 大模型 | Agent |
|---|---|---|
| 核心能力 | 文本生成 | 任务执行 |
| 主动性 | 被动响应 | 主动规划 |
| 工具使用 | 需要人工指定 | 自主决定调用 |
| 任务处理 | 单轮对话 | 多步骤任务流 |
| 记忆能力 | 有限(Context限制) | 可扩展(外部存储) |
| 错误处理 | 无 | 可重试、调整策略 |
9.4 Agent工作流程示例
场景:用户让Agent帮忙订一张去北京的机票
步骤1:理解意图
Agent分析用户需求:"订去北京的机票"
步骤2:规划任务
Agent把任务拆解:
- 确定出发城市
- 确定出发日期
- 查询航班信息
- 选择合适航班
- 完成预订
步骤3:执行与交互
- Agent问:"请问您从哪个城市出发?"
- 用户答:"上海"
- Agent问:"请问您希望什么日期出发?"
- 用户答:"明天"
- Agent调用航班查询工具
- Agent展示查询结果
- Agent问:"您选择哪个航班?"
- 用户选择后,Agent调用预订工具
步骤4:反馈与确认
Agent返回:"已为您预订成功,订单号是..."
8.5 SubAgent(子智能体)
什么是SubAgent?
SubAgent是Agent的子智能体,用于处理Agent任务流程中的子任务。
Agent vs SubAgent的区别
| 对比维度 | Agent | SubAgent |
|---|---|---|
| 定位 | 主控智能体 | 辅助智能体 |
| 任务范围 | 完整任务 | 子任务 |
| 调用关系 | 被用户调用 | 被Agent调用 |
| 生命周期 | 长期存在 | 任务完成后销毁 |
| 决策权 | 高 | 低(由Agent分配) |
SubAgent使用场景
例子:用户让Agent写一个技术文档
- Agent接收到任务:"写一份API文档"
-
Agent规划:
- 调用SubAgent1:分析代码,提取API接口信息
- 调用SubAgent2:生成文档模板
- 调用SubAgent3:填充具体内容
- SubAgent1完成代码分析,返回接口列表
- SubAgent2生成文档结构
- SubAgent3根据接口信息填充文档内容
- Agent整合所有结果,返回最终文档
一句话总结:Agent = 能自己决定怎么做事的大模型
8.6 多Agent协同模式
为什么需要多Agent协同?
单个Agent虽然强大,但面对复杂任务时,往往需要"术业有专攻"。通过多个Agent协同工作,可以实现:
- 专业分工:每个Agent专注自己的领域
- 质量提升:通过互相检查、辩论,减少错误
- 效率优化:并行处理多个子任务
- 能力互补:不同Agent拥有不同的工具和知识
动态规划示例:
- 代码审查Agent:挂载代码分析工具,使用Claude模型(擅长代码理解)
- 数据分析Agent:挂载数据处理工具,使用GPT-4模型(擅长数学推理)
- 文档写作Agent:挂载文档模板工具,使用Gemini模型(擅长长文本生成)
三种主流协同模式
![]()
一、上下级协同(Hierarchical)——类比公司组织架构
结构:
![]()
工作方式:
- 中控Agent负责拆解任务、分配工作、整合结果
- 子Agent负责具体执行
- 可以多层嵌套,形成树状结构
适用场景:
- 大型项目管理
- 复杂系统的开发
- 需要严格流程控制的任务
实际案例 - 飞猪行程规划:
- 中控Agent:接收用户"规划一次北京到上海的旅行"需求
- SubAgent1:查询北京到上海的航班信息
- SubAgent2:搜索上海的酒店和景点
- SubAgent3:生成详细的行程安排
- 中控Agent:整合所有信息,输出完整行程单
实际案例 - 机票预订:
- 中控Agent:接收"预订明天北京到上海的机票"需求
- SubAgent1:查询航班信息(时间、价格、航空公司)
- SubAgent2:对比不同航班的性价比
- SubAgent3:执行预订操作
- 中控Agent:确认预订结果,返回订单号
二、师生式协同(Master-Disciple)——本质是"带思路"
结构:
工作方式:
- 专家Agent提供策略、方法论、评价标准
- 新手Agent按照专家的指导执行具体任务
- 专家可以实时反馈和调整
关键要素:
- 策划思路:如何拆解问题、制定策略
- 信息收集方法:从哪里获取信息、如何筛选有效信息
- 表达格式规范:输出应该是什么格式、包含哪些要素
- 评价标准:如何判断结果的好坏、如何改进
适用场景:
- 需要传承专业知识的任务
- 质量要求高的内容创作
- 需要标准化流程的工作
实际案例 - 单轮对话优化:
- 用户输入:"帮我写一个产品介绍"
- 新手Agent:生成初步版本(可能不够专业)
- 专家Agent:提供反馈:"需要突出产品的核心优势,增加数据支撑,使用更专业的术语"
- 新手Agent:根据反馈优化输出
- 专家Agent:继续指导:"结构可以调整为:问题背景 → 产品解决方案 → 核心优势 → 客户案例"
- 新手Agent:按照新结构重新生成
- 最终输出:高质量的产品介绍
本质区别:上下级协同是主智能体严格拆解任务并分配,师生式协同则是通过讨论和反馈优化输出,更具互动性。
三、竞争式协同(Competitive / Debate)——让模型"互相杠"
结构:
工作方式:
- 多个Agent独立生成不同方案
- 裁判Agent对比分析各方案优劣
- 选择最优或融合多个方案
本质:多解 → 对比 → 选择最优
适用场景:
- 开放性问题(没有标准答案)
- 需要高质量输出的任务
- 容易"幻觉"的任务(需要互相验证)
- 创意类工作(需要多个视角)
实际案例 - 营销文案创作:
- 任务:"为一款智能手表写营销文案"
- Agent1:强调性价比(价格优势、功能对比)
- Agent2:强调品质(材质、工艺、品牌背书)
- Agent3:强调创新(独特功能、技术突破)
-
裁判Agent:综合三个角度,生成最优文案
- 开头用创新点吸引注意力
- 中间用品质数据建立信任
- 结尾用性价比促成转化
为什么竞争式协同有效?
- 避免单一视角:不同Agent从不同角度思考,避免思维局限
- 互相验证:多个方案可以互相检查,减少幻觉和错误
- 激发创意:竞争机制激发Agent的创造力,产生更好的想法
- 质量提升:通过对比筛选,最终输出质量更高
应用场景总结
多智能体协作适用于复杂任务(如工程开发、项目上线),通过分工减轻单一智能体负担。
| 应用场景 | 推荐模式 | 理由 |
|---|---|---|
| 工程开发 | 上下级协同 | 需要严格的任务拆解和流程控制 |
| 项目上线 | 上下级协同 | 涉及多个环节,需要统一调度 |
| 代码审查 | 师生式协同 | 需要专家指导,逐步优化代码质量 |
| 内容创作 | 师生式协同 | 需要反复打磨,提升内容质量 |
| 方案设计 | 竞争式协同 | 需要多角度思考,选择最优方案 |
| 创意生成 | 竞争式协同 | 需要激发创意,避免思维固化 |
| 数据分析 | 混合模式 | 用上下级协同拆解任务,用竞争式协同验证结果 |
如何选择协同模式?
| 任务特点 | 推荐模式 |
|---|---|
| 复杂、需要严格流程 | 上下级协同 |
| 需要专业知识传承 | 师生式协同 |
| 开放性、创意类 | 竞争式协同 |
| 需要多角度验证 | 竞争式协同 |
| 大型项目管理 | 上下级协同 |
| 需要迭代优化 | 师生式协同 |
一句话总结:多Agent协同 = 让多个专业AI各司其职,通过分工、合作、竞争,共同完成复杂任务
十、思考题
Q1:Token和字符有什么区别?为什么大模型不直接处理字符?
- Token是大模型处理文本的最小单位,Token和字符不是一对一关系
- 一个Token可能对应一个词、多个字符,也可能一个字符对应多个Token
- 平均1个Token ≈ 1.5~2个汉字,或0.75个英文单词
- 不直接处理字符的原因:字符粒度太细,模型需要学习更多模式;Token粒度更符合语言单元,训练效率更高
Q2:Context Window越大越好吗?
- Context Window是大模型一次能处理的最大Token数量
- 它决定了模型能"记住"多少信息
- 影响模型的成本和性能
- 不同模型的Context Window大小不同(GPT-4:128K,Claude 3.1 Pro:100万)
- 不是越大越好:更大的Context意味着更高的计算成本,需要根据实际需求选择合适的模型
Q3:RAG和Fine-tuning有什么区别?什么时候用哪个?
- RAG:检索时动态注入知识,适合知识频繁更新的场景
- Fine-tuning:将知识嵌入模型参数,适合特定任务的优化
- RAG优势:知识可实时更新,成本低,不受Context Window限制
- Fine-tuning优势:模型对特定领域更熟悉,输出更稳定
- 两者可以结合使用:用Fine-tuning学习领域风格,用RAG获取最新知识
Q4:Agent和普通的大模型有什么本质区别?
- Agent是具备自主规划和执行能力的智能体
- 区别:
- 大模型:被动响应,只能生成文本
- Agent:主动规划,可以执行多步骤任务
- Agent的核心能力:感知、规划、执行、反思
- Agent可以自主决定何时调用工具、调用哪个工具
- Agent不是更聪明,是更会做事
Q5:Agent未来的发展方向是什么?
技术方向:
- 多模态能力:处理文本、图像、音频、视频
- 更强的规划能力:处理更复杂的任务链
- 自主学习:根据反馈优化自身行为
- 协作能力:多个Agent协同工作
应用方向:
- 垂直领域Agent:医疗、法律、金融等
- 个人化Agent:深度了解用户习惯和偏好
- 企业级Agent:处理复杂的业务流程
- 物理世界Agent:机器人、自动驾驶等
挑战:
- 安全性:防止Agent执行危险操作
- 可控性:确保Agent行为符合预期
- 成本:降低Agent运行成本
- 普适性:让更多普通人能使用Agent
十一、总结
核心概念回顾
| 概念 | 英文 | 定义 |
|---|---|---|
| 大语言模型 | LLM | 基于Transformer架构训练的大规模语言模型 |
| 词元 | Token | 大模型处理文本的最小单位 |
| 上下文 | Context | 大模型每次处理任务时接收的信息总和 |
| 上下文窗口 | Context Window | Context能容纳的最大Token数量 |
| 提示词 | Prompt | 大模型接收的具体问题或指令 |
| 检索增强生成 | RAG | 从大量文档中检索相关片段发给大模型的技术 |
| 工具 | Tool | 让大模型能够执行具体任务的函数 |
| 智能体 | Agent | 具备自主规划和执行能力的AI系统 |
| 子智能体 | SubAgent | 被Agent调用的辅助智能体 |
| 技能 | Skill | 针对特定任务的预配置能力包 |
| 模型上下文协议 | MCP | 连接大模型和外部数据源的标准化协议 |
![]()
十二、结语
AI技术日新月异,但核心概念始终如一。从最底层的LLM、Token,到中层的Context、Prompt、RAG,再到上层的Tool、Agent、Skill、MCP,这些概念构成了现代AI应用的技术栈。
希望这篇文章能帮助你建立扎实的AI知识体系。如果觉得有用,欢迎分享给更多需要的朋友!
注:本文内容基于当前主流AI技术整理,随着技术发展,部分概念可能会有更新。建议持续关注最新动态。
Vite:比Webpack快100倍的“闪电侠”,原理竟然这么简单?
听说Vite很快?快得像你点下保存,浏览器立马刷新。今天我们就来拆解这个“前端新宠”,看看它到底用了什么黑魔法。看完你会发现:哦,原来不是魔法,是“降维打击”!
前言
Webpack就像个勤劳的蚂蚁,把整个项目一点点搬完再给你看结果。Vite则像个聪明的快递员:你点什么,它送什么,绝不提前扛一堆货。
Vite(法语“快”)是尤雨溪推出的构建工具,开发服务器启动快到“秒开”,热更新快到“没感觉”。它的秘诀就是:利用浏览器原生ES模块(ESM),让浏览器帮你分担工作,自己只做最轻量的事。
一、Webpack为什么慢?因为它在“提前打包”
Webpack的工作方式是:启动开发服务器时,要从入口开始,把整个项目的所有模块都打包成一个(或几个)bundle。即使你用lazy loading,它也要先解析所有依赖关系。
这个过程随着项目变大而变慢。想象一下,你只是改了一行代码,Webpack却要重新打包一大坨东西——虽然做了缓存,但还是很重。
二、Vite的思路:让浏览器做打包
现代浏览器已经支持<script type="module">,可以直接在HTML里导入ES模块,浏览器会按需请求每个模块文件。
Vite利用这一点:开发时,它不打包,而是直接把源码转换成ES模块,让浏览器去请求。当浏览器请求/src/main.js时,Vite拦截请求,实时编译(比如把JSX转成JS,把TS转成JS),然后返回给浏览器。
启动速度对比
- Webpack:启动 = 打包整个应用 → 慢。
- Vite:启动 = 启动一个静态服务器 + 预构建第三方依赖 → 极快。
热更新(HMR)对比
- Webpack:改一个模块,可能需要重新打包部分bundle。
- Vite:利用ESM的精确依赖关系,只更新被改的模块,浏览器只需要重新请求那个模块,速度飞起。
三、Vite的核心原理:三大绝招
1. 基于原生ESM的开发服务器
Vite在开发环境下,把每个文件都当作独立的模块,通过koa或connect启动一个服务器。当浏览器请求/src/App.vue时,Vite会实时编译Vue组件,返回一个JS模块。
// 浏览器请求 main.js
import { createApp } from '/node_modules/.vite/deps/vue.js'
import App from '/src/App.vue'
createApp(App).mount('#app')
注意,import里的路径是相对于服务器的,Vite会拦截并处理。
2. 预构建:用esbuild把第三方库“打成一块”
虽然Vite不打包业务代码,但第三方库(比如vue、react)往往有很多内部模块。如果让浏览器一个个请求,请求数太多,性能差。
Vite在启动时会用esbuild(用Go写的,超快)把第三方库预构建成单个ESM模块,然后缓存起来。这样浏览器只需要请求一个文件,而不是几十个。
3. 按需编译 + 缓存
Vite只会编译浏览器实际请求的文件。你没访问到的页面组件,Vite根本不会编译。而且编译结果会缓存到node_modules/.vite,下次启动秒开。
四、生产环境:还是Rollup打包
有人问:Vite开发那么快,生产环境也用ESM不就行了?问题是,纯ESM在生产环境下有太多请求,性能不好。而且需要做tree shaking、代码分割、压缩等优化。
所以Vite在生产环境默认使用Rollup打包,打包出高度优化的静态文件。这样既享受了开发时的快,又保证了生产时的优。
五、手写一个迷你Vite
我们来模拟Vite的核心思想:一个按需编译的静态服务器。
// mini-vite.js
const express = require('express');
const fs = require('fs');
const path = require('path');
const { transform } = require('esbuild'); // 用esbuild转译
const app = express();
app.use((req, res, next) => {
const url = req.url;
if (url === '/') {
// 返回index.html
const html = fs.readFileSync('./index.html', 'utf-8');
res.setHeader('Content-Type', 'text/html');
return res.send(html);
}
// 处理JS文件
if (url.endsWith('.js')) {
const filePath = path.join(__dirname, url);
const content = fs.readFileSync(filePath, 'utf-8');
// 用esbuild转译JSX/TS等(简化版)
transform(content, { loader: 'jsx' }).then(result => {
res.setHeader('Content-Type', 'application/javascript');
res.send(result.code);
});
} else {
next();
}
});
app.listen(3000, () => console.log('Mini Vite running at http://localhost:3000'));
这只是个玩具,真实Vite复杂得多(处理Vue单文件、HMR、预构建等),但核心思想一致:拦截请求、实时转换、返回ES模块。
六、Vite的适用场景和坑点
适用场景
- 新项目,尤其使用Vue3或React+TS
- 需要极快开发体验
- 对构建配置要求不高(默认配置够用)
坑点
- 依赖CommonJS格式的库可能有问题(Vite会尝试转换,但少数不行)
- 动态导入
import()的路径必须静态可分析 - 生产环境用Rollup,配置和开发环境可能不一致(但Vue官方推荐)
七、总结:Vite不是魔法,是“借力”
- 开发时:利用浏览器ESM + esbuild预构建 + 按需编译 → 秒启、快更。
- 生产时:Rollup打包 → 优化产物。
- 核心思想:让浏览器做更多,服务器做更少。
Webpack正在努力追赶(比如Webpack5的模块联邦和缓存),但Vite的“降维打击”思路确实带来了革命性的开发体验。如果你还没试过,去创建一个Vite项目体验一下,你会回来点赞的。
如果你觉得今天的“闪电侠”够形象,点个赞让更多人看到。明天我们将进入TypeScript基础,从类型注解到接口,让你写出更健壮的代码。我们明天见!
Connect 深度解析:Node.js 中间件框架的基石
在 Node.js 生态中,connect 是一个轻量级、可扩展的 HTTP 中间件框架。它虽然代码量不大(核心文件仅数百行),却奠定了 Express、Koa 等现代 Web 框架的中间件设计基础。理解 connect 的源码与设计思想,有助于掌握 Node.js HTTP 开发的底层模式。本文将从概念、使用方法、源码实现、中间件机制以及应用场景五个维度,对 connect 进行全面剖析。
Connect 是什么?
connect ,其定位是“Node.js 的中间件层”。它本身不是一个完整的 Web 框架,而是一个可插拔的 HTTP 请求处理管道。开发者可以将各种功能(日志、静态文件、路由、代理等)以中间件的形式插入到管道中,按顺序处理请求。
connect 的核心概念:
-
中间件:一个接受
(req, res, next)的函数,可以修改请求/响应、结束请求或调用下一个中间件。 -
中间件栈:使用
use方法注册中间件,形成一个数组(栈),请求到来时依次执行。 -
错误处理:通过
(err, req, res, next)形式的中间件捕获异常。
Connect 的核心是其维护的一个中间件队列(stack),通过use方法将中间件注册到队列中。每个中间件都是一个函数,它能够访问请求对象(req)、响应对象(res)以及控制权传递函数(next)。
中间件处理的核心在于next()函数:
-
调用
next():表示当前中间件已完成处理,将控制权传递给队列中的下一个中间件。 -
不调用
next():表示请求处理链终止,不再继续向下执行。
这种机制确保了每一个中间件只处理它负责的部分,实现了职责分离和灵活组合。
Connect 使用
1、初始化与基础设置
const connect = require("connect");
const http = require("http");
const app = connect();
const server = http.createServer(app);
server.listen(3000);
console.log("Server is running on port 3000");
2、不指定路径,中间件会对每个请求执行
app.use(function logger(req, res, next) {
const start = Date.now();
const originalUrl = req.url; // 保存原始 URL
// 监听响应结束事件(因为 res.end 是异步的)
res.on("finish", () => {
const duration = Date.now() - start;
console.log(`响应结束 ${req.method} ${originalUrl} - ${res.statusCode} - ${duration}ms`);
});
next();
});
3、路径匹配中间件
请求路径以 /user 开头时触发(如 /user、/user/profile)
app.use("/user", (req, res, next) => {
console.log("用户中间件");
res.setHeader("Content-Type", "text/plain");
res.end("User area");
});
4、子应用挂载
const adminApp = connect();
adminApp.use((req, res, next) => {
res.setHeader("Content-Type", "text/plain");
res.end("Admin area");
});
app.use("/admin", adminApp);
5、错误触发中间件
访问 /error 时主动抛出错误,用于测试错误处理中间件。
app.use("/error", (req, res, next) => {
throw new Error("Error");
});
5、404中间件
所有未匹配的请求都会返回 404 Not Found。
app.use((req, res) => {
res.statusCode = 404;
res.end('Not Found');
});
7、错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack);
res.statusCode = 500;
res.end("Internal Server Error");
});
源码 connect@3.7
/*!
* connect
* Copyright(c) 2010 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
'use strict';
/**
* Module dependencies.
* @private
*/
var debug = require('debug')('connect:dispatcher');
var EventEmitter = require('events').EventEmitter;
var finalhandler = require('finalhandler');
var http = require('http');
var merge = require('utils-merge');
var parseUrl = require('parseurl');
/**
* Module exports.
* @public
*/
module.exports = createServer;
/**
* Module variables.
* @private
*/
var env = process.env.NODE_ENV || 'development';
var proto = {};
/* istanbul ignore next */
var defer = typeof setImmediate === 'function'
? setImmediate
: function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
/**
* Create a new connect server.
* 用于创建一个新的 Connect 服务器实例。
* 这个函数返回一个具有中间件处理能力的函数,该函数可以作为 HTTP 服务器的请求处理器
*
* @return {function}
* @public
*/
function createServer() {
// 创建 app 函数
// 当 app 函数被调用时,它会调用自身的 handle 方法来处理请求
function app(req, res, next){ app.handle(req, res, next); }
// 将 proto 对象的属性合并到 app 函数上
merge(app, proto);
merge(app, EventEmitter.prototype);
app.route = '/';
app.stack = []; // 存储中间件函数
return app;
}
/**
* Utilize the given middleware `handle` to the given `route`,
* defaulting to _/_. This "route" is the mount-point for the
* middleware, when given a value other than _/_ the middleware
* is only effective when that segment is present in the request's
* pathname.
*
* For example if we were to mount a function at _/admin_, it would
* be invoked on _/admin_, and _/admin/settings_, however it would
* not be invoked for _/_, or _/posts_.
*
* @param {String|Function|Server} route, callback or server
* @param {Function|Server} callback or server
* @return {Server} for chaining
* @public
*/
// 向中间件栈中添加一个新的中间件
proto.use = function use(route, fn) {
var handle = fn;
var path = route;
// default route to '/'
// 函数重载:如果 route 参数不是字符串,说明是中间件函数,直接赋值给 handle 变量
// 不指定路径,中间件会对每个请求执行。
if (typeof route !== 'string') {
handle = route;
path = '/';
}
// wrap sub-apps
// 子应用包装(Sub-app)
// 传入的 handle 是一个 connect 应用实例(具有 handle 方法),则将其包装成一个中间件函数
if (typeof handle.handle === 'function') {
var server = handle; // 子应用实例
server.route = path;
// 中间件函数
handle = function (req, res, next) {
server.handle(req, res, next);
};
}
// wrap vanilla http.Servers
// HTTP 服务器适配
// 传入的 handle 是 Node.js 原生 http.Server 实例,则提取其 'request' 事件监听器(即第一个处理函数)作为中间件
if (handle instanceof http.Server) {
handle = handle.listeners('request')[0];
}
// strip trailing slash
// 移除路径末尾的斜杠,确保路径格式为 /admin 而不是 /admin/
if (path[path.length - 1] === '/') {
path = path.slice(0, -1);
}
// add the middleware
debug('use %s %s', path || '/', handle.name || 'anonymous');
// 将中间件对象(包含 route 和 handle)推入 this.stack 数组
this.stack.push({ route: path, handle: handle });
return this;
};
/**
* Handle server requests, punting them down
* the middleware stack.
* 遍历中间件栈(this.stack),根据请求路径匹配中间件,并依次执行
*
* @private
*/
proto.handle = function handle(req, res, out) {
var index = 0; // 当前中间件在栈中的索引
// 请求 URL 中的协议+主机部分(如 http://example.com)
var protohost = getProtohost(req.url) || '';
var removed = ''; // 记录已被匹配并“剥离”的路由前缀
var slashAdded = false; // 标记是否因为路径变换而添加了前导斜杠
var stack = this.stack;
// final function handler
// 最终处理函数(默认 finalhandler),当所有中间件执行完或出错时调用
var done = out || finalhandler(req, res, {
env: env,
onerror: logerror
});
// store the original URL
// 保存原始请求 URL
req.originalUrl = req.originalUrl || req.url;
function next(err) {
// 1、恢复 URL 变换
// 因为匹配路由而临时去掉了前导斜杠(slashAdded === true),则将其加回
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
// 之前剥离了路由前缀(removed 非空),则将其重新拼接到 req.url 前面
if (removed.length !== 0) {
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
}
// next callback
// 2、取出当前中间件,索引自增
var layer = stack[index++];
// all done
// 如果已无中间件,则调用 done(可能传递错误),结束处理
if (!layer) {
defer(done, err);
return;
}
// 3、路径匹配检查
// route data
var path = parseUrl(req).pathname || '/';
var route = layer.route;
// skip this layer if the route doesn't match
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}
// skip if route match does not border "/", ".", or end
// 4、边界符检查
var c = path.length > route.length && path[route.length];
if (c && c !== '/' && c !== '.') {
return next(err);
}
// 5、URL 变换(剥离路由前缀)
// trim off the part of the url that matches the route
if (route.length !== 0 && route !== '/') {
removed = route;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// ensure leading slash
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
}
// 6、调用中间件
// call the layer handle
call(layer.handle, route, err, req, res, next);
}
next();
};
/**
* Listen for connections.
*
* This method takes the same arguments
* as node's `http.Server#listen()`.
*
* HTTP and HTTPS:
*
* If you run your application both as HTTP
* and HTTPS you may wrap them individually,
* since your Connect "server" is really just
* a JavaScript `Function`.
*
* var connect = require('connect')
* , http = require('http')
* , https = require('https');
*
* var app = connect();
*
* http.createServer(app).listen(80);
* https.createServer(options, app).listen(443);
*
* @return {http.Server}
* @api public
*/
proto.listen = function listen() {
// 1、创建 HTTP 服务器实例
// this 指向 connect 应用实例
// http.createServer(this) 创建原生 HTTP 服务器,并将 app 作为请求监听器
// 这意味着每当有 HTTP 请求到达时,就会调用 app(req, res),从而进入 Connect 的中间件处理管道
var server = http.createServer(this);
// 2、启动 HTTP 服务器
return server.listen.apply(server, arguments);
};
/**
* Invoke a route handle.
* @private
*/
function call(handle, route, err, req, res, next) {
var arity = handle.length;
var error = err;
var hasError = Boolean(err);
debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);
try {
if (hasError && arity === 4) {
// error-handling middleware
handle(err, req, res, next);
return;
} else if (!hasError && arity < 4) {
// request-handling middleware
handle(req, res, next);
return;
}
} catch (e) {
// replace the error
error = e;
}
// continue
next(error);
}
/**
* Log error using console.error.
*
* @param {Error} err
* @private
*/
function logerror(err) {
if (env !== 'test') console.error(err.stack || err.toString());
}
/**
* Get get protocol + host for a URL.
* 从 URL 字符串中提取协议和主机部分(protocol + host)
*
* @param {string} url
* @private
*/
function getProtohost(url) {
// 检查 URL 是否为空字符串或以 '/' 开头
// 这类 URL 通常是相对路径,不包含协议和主机信息
if (url.length === 0 || url[0] === '/') {
return undefined;
}
// 查找 URL 中的协议分隔符位置
var fqdnIndex = url.indexOf('://')
return fqdnIndex !== -1 && url.lastIndexOf('?', fqdnIndex) === -1
? url.substr(0, url.indexOf('/', 3 + fqdnIndex))
: undefined;
}
AI 写代码总翻车?我用 Harness:developer 把它管成“右侧打工人”
先说一个真实场景。
你让 AI 改个功能,本来想要“10 分钟提效”,结果最后变成“2 小时排雷”:
- 一会儿改了不该改的文件;
- 一会儿说“已完成”但没有验证证据;
- 你回头想复盘,发现没有计划、没有过程、没有日志;
- 最终你和同事围着屏幕沉默三分钟,谁都不敢点合并。
我后来发现,问题不在“模型笨”,而在“流程野”。
于是我设计了一个任务编排skill:Harness:developer, 干的事很直接:
把 AI 开发变成一条有门禁的流水线。主会话只负责编排,右侧 pane 才能写业务代码。你可以理解成让 AI 去右边工位打卡上班,左边主管只盯流程,不准亲自抡键盘抢活。
这篇文章不讲玄学,直接讲它到底怎么工作。看完你就能按步骤跑,尤其适合刚上手 AI 协作开发的同学。
先记住一句总规则
从 Step 2 开始,到 Step 6 结束:主会话禁止业务编码。
是的,禁止。不是“尽量不要”,是“违规就 fail”。
为什么这么绝对?因为大多数事故都发生在“差不多就行”的灰色地带。主会话一旦开始手改业务代码,你后面就很难分清:到底是编排成功,还是救火成功。
整条流程长什么样
Harness:developer 固定是 9 个阶段:
- Step 0:Preflight:环境预检
- Step 1:需求理解与计划落盘:prd/在线原型/原型源码三路并行分析
- Step 2:编排锁建立:claudecode使用agent team分析需求并执行计划,codex使用subagents分析需求并执行计划
- Step 3:启动指令生成
- Step 4:启动右侧编码会话
- Step 5:任务下发 + ACK 校验
- Step 6:tmux+smux右侧创建pane,执行编码(主会话只监控,开发过程实时记录进度)
- Step 7:强制调用
/Harness:verify验证编码结果 - Step 8:日志归档 + 规则合规报告
你可以把它看成“开发版过安检”。每一步都要验票,没票就别进下一站。
开始流程
![]()
终端执行tmux -> codex --yolo 或者 claude --dangerously-skip-permissions -> 输入提示词
# claudecode
/Harness:developer "完成 @docs/产品文档/v1.0/产品文档/交易中心_分账订单PRD.md" "codex" "http://localhost:3000/#/payment-orders/transfers"
# codex
$Harness:developer "完成 @docs/产品文档/v1.0/产品文档/交易中心_分账订单PRD.md" "codex" "http://localhost:3000/#/payment-orders/transfers"
Step 0:先查体检,再谈开工
![]()
这一阶段主要确认“你现在是不是在能跑流程的环境里”。
要检查的核心点:
- tmux 是否可用,主 pane 能不能探测到
-
smux/tmux-bridge是否可用 - 当前会话识别为
codex还是claude - 关键 skill 是否具备:
/prototype-reader、/Harness:progress、/Harness:verify
这一步最常见的翻车是:根本不在 tmux 里,或者 pane 信息都拿不到,还硬着头皮继续。后面自然是连锁崩。
一句话总结:Step 0 不是形式主义,它是“这趟车能不能发车”的决定点。
Step 1:三路并行分析,不再盲改
![]()
这里是我认为最值钱的一步。Harness:developer 要求并行跑 3 个分析单元:
-
prd-analyst:看需求 -
prototype-explorer:看原型 -
source-analyst:看现有代码
注意,原型分析必须给硬证据,不接受“我看过了”的口头保证。至少要有:
- 页面开闭记录
- 3 张及以上截图(列表页、详情/弹窗、配置页)
- 浏览器已清理标记
- 主会话逐项验证截图文件存在
这一步完成后,会把计划文档和进度文档落盘。也就是说,从这一刻开始,你不再是“脑内计划”,而是“可追踪计划”。
![]()
![]()
很多人跳过这一步,理由是“我急着写代码”。结果往往是后面改三轮,最后总耗时更长。
Step 2:上锁,正式进入“只编排模式”
![]()
Step 2 是整条链路的硬门禁。
这里要做几件关键事:
- 校验 Step 1 产物存在(计划、进度、分析单元已回收)
- 输出固定强提醒:Step2-6 主会话仅编排,违规即 fail
- 识别并记录
MAIN_PANE_ID - 清理多余子 pane
- 建立
ORCHESTRATION_LOCK=on并写锁文件
这把锁的意义非常现实:防止流程运行中途“主会话忍不住下场改代码”。
你可以把它当作“防手痒机制”。听起来有点好笑,但真有用。
Step 3:生成启动命令,做一次防串改
这一步会生成 READY_TOKEN 和 START_CMD,并做可执行校验。
关键要求是:START_CMD 必须包含 ready:{READY_TOKEN}。
如果只是裸 ready,后面不认。因为裸 ready 很容易误判来源,token 化回执才能明确“这是这次任务、这个 pane、这条链路的回执”。
此外还会记录 START_CMD_SHA1,防止命令被串改或发错版本。
Step 4:拉起右侧 pane,确认“打工人已就位”
动作看起来简单:
- 新建右侧 pane
- 拿到
TARGET_PANE_ID - 确认它不等于
MAIN_PANE_ID - 通过协议发送启动命令
- 等待
ready:{READY_TOKEN} - 校验 pane 当前进程必须是
codex|claude
真正难的不是“会不会 split-window”,而是“会不会做来源校验”。
我见过最经典的事故是:ready 回执出现在主 pane,但大家没注意,还继续推进。最后你以为右侧在写代码,实际右侧压根没起来。
这就是为什么它要求 token、要求来源、要求进程在位校验。不是麻烦,是防事故。
Step 5:下发任务,必须拿到 ACK 才算派工成功
![]()
这一步的核心是“发送任务 + 验收回执”。
合规 ACK 形态主要是:task_received:{MODULE_NAME}。
如果走 fallback,也不是随便说一句“我收到了”就行,还要补齐工作态证据,比如已经进入工作、已返回空闲等。
还有个细节很关键:ACK 必须来自目标 pane。主 pane 出现对应 ACK,直接判误发。
你可以理解成工单系统里的“签收回执”:没签收,不算派单成功。
Step 6:右侧执行编码,主会话只监控
![]()
这是最考验耐心的一段。
主会话此时允许做的事只有三类:
- 监控目标 pane 输出
- 定时记进度(通常每 3 分钟一次)
- 必要时发“继续开发”
不允许做的事就一条,但非常重要:主会话不能接管业务编码。
并且完成回执有顺序要求:
verify_done:{MODULE_NAME}done_ack:{MODULE_NAME}
顺序错了不行,缺一个也不行。
另外,流程把“等待态”和“故障态”分得很清楚:
-
timeout、task_blocked:*:属于等待态,继续等 -
429/50x:才算故障态,可进入恢复策略
这条规则能显著减少“焦虑型误操作”:看着不动就 kill pane,结果把正常执行链路硬切断。
定时更新进度的效果图:
![]()
![]()
Step 7:必须走 /Harness:verify,不能口头毕业
![]()
到了这步,很多人会说“我自己跑了 lint/typecheck 就好了吧”。
不行。
Harness:developer 要求必须调用 /Harness:verify,并保留可核验回执。原因很简单:统一口径、统一证据、统一复盘入口。否则每个人都说“我测过了”,但没人说得清“你到底怎么测的”。
Step 8:归档日志,让这次开发可追溯
![]()
最后一步是很多团队最容易忽略的,但长期价值最大:
- 写自检日志到固定目录
- 记录计划执行、验证结果、规则检查、问题修复
- 输出 Rule Compliance Report(改动文件 -> 对应规则)
这一步做得好,后面查问题就不是“靠记忆猜”,而是“按记录查”。
这套流程到底值不值得
如果你只看“第一次上手成本”,它确实比一句 prompt 重。
但如果你看“总交付成本”,它通常更省:
- 需求理解更早收敛,少返工
- 过程责任更清晰,少扯皮
- 验证证据更统一,少争议
- 归档更完整,少失忆
说白了,这是一套“把 AI 开发从表演赛变成联赛”的方法。
给小白的落地建议
如果你今天就想试,照这个顺序来:
- 先完整跑一遍,不要私自删 Step
- 严格执行 Step 2 的锁,不要觉得自己能自律
- 把 Step 1 的原型证据当硬指标,不要口头化
- Step 7 一定走
/Harness:verify,别手动替代 - Step 8 认真写日志,给未来的自己省时间
你会发现,流程不是束缚,而是“降低犯错自由度”。这在多人协作里,几乎总是好事。
结尾
Harness:developer 最厉害的地方,不是让 AI 写得更快,而是让你知道“它到底有没有按规范写”。
快不快是一时的,稳不稳是长期的。
把 AI 放到右侧 pane 去打工,把主会话留给编排和审计。你会明显感觉到:项目开始像项目了,不再像临场 improvisation。
为了搞懂 Promise 源码,我重写了 MiniPromise
前言
Promise 源码看了一百遍,不如自己写一遍。
相信很多前端同学都有过这样的经历:面试问手写 Promise,网上搜一搜 "Promise A+ 规范实现",然后对着代码 Copy 一遍,写完还是云里雾里 —— 那些 .then、Promise.resolve、Promise.all 到底是怎么串起来的?
这篇文章不打算贴完整代码(GitHub 上已经够多了),而是换个方式:用一个最小、最简、最裸的 MiniPromise,带你从零理解 Promise 的设计思路。
1. 先想清楚:Promise 解决什么问题?
在 Promise 出现之前,我们用回调函数来处理异步:
fetchData(function(result) {
processResult(result, function(processed) {
saveData(processed, function() {
// ... 回调地狱
});
});
});
这叫 回调地狱(Callback Hell),问题不仅仅是嵌套难读,更重要的是 错误处理分散、状态不可控。
Promise 的核心思路就两点:
- 状态机:pending → fulfilled / rejected,只能变一次
-
链式调用:
.then()返回一个新的 Promise,实现"异步组合"
理解这两点,你就能自己动手写一个简化版 Promise。
2. MiniPromise 的核心结构
class MiniPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => this._resolve(value);
const reject = (reason) => this._reject(reason);
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
}
就这?一个类,三个属性,两个数组?
对,就是这么裸。Promise 本质就是一个状态容器,所有的魔法都在 _resolve 方法里。
3. 核心逻辑:状态流转 + 异步执行
_resolve(value) {
// 只能从 pending 变一次
if (this.state !== 'pending') return;
// 如果 value 是 Promise,需要"展开"
if (value instanceof MiniPromise) {
return value.then(this._resolve.bind(this), this._reject.bind(this));
}
this.state = 'fulfilled';
this.value = value;
// 异步执行回调 —— 这就是 then 可以链式调用的关键
queueMicrotask(() => {
this.onFulfilledCallbacks.forEach(cb => cb(this.value));
});
}
等等,这里有个关键点:为什么要用 queueMicrotask?
因为 Promise 的设计原则是:then 的回调必须异步执行。这保证了执行顺序的可预测性,也是 Promise/A+ 规范的要求。
4. then 是怎么实现的?
then(onFulfilled, onRejected) {
// 返回一个新的 Promise,这就是链式调用的秘密
return new MiniPromise((resolve, reject) => {
const handleCallback = (callback, value) => {
try {
// 如果没有传回调,直接透传 value
const result = callback ? callback(value) : value;
resolve(result); // 关键:resolve 的是回调的返回值
} catch (err) {
reject(err);
}
};
if (this.state === 'fulfilled') {
// 异步执行,保持一致性
queueMicrotask(() => handleCallback(onFulfilled, this.value));
} else if (this.state === 'rejected') {
queueMicrotask(() => handleCallback(onRejected, this.value));
} else {
// pending 状态,先把回调存起来
this.onFulfilledCallbacks.push(() => handleCallback(onFulfilled, this.value));
this.onRejectedCallbacks.push(() => handleCallback(onRejected, this.value));
}
});
}
看到没?then 返回的是一个全新的 Promise,而不是直接返回结果。这个新 Promise 的 resolve 取决于回调函数的返回值——这,就是链式调用的本质。
5. 静态方法:Promise.resolve / Promise.reject
static resolve(value) {
if (value instanceof MiniPromise) return value;
return new MiniPromise(resolve => resolve(value));
}
static reject(reason) {
return new MiniPromise((_, reject) => reject(reason));
}
简单到不用解释。
6. Promise.all 怎么写?
static all(promises) {
return new MiniPromise((resolve, reject) => {
const results = [];
let completed = 0;
if (promises.length === 0) return resolve([]);
promises.forEach((p, i) => {
MiniPromise.resolve(p).then(val => {
results[i] = val;
completed++;
if (completed === promises.length) resolve(results);
}, reject);
});
});
}
核心就一个:遍历 + 计数 + 全部成功才 resolve。
7. 写完 MiniPromise,我学到了什么?
-
Promise 不是什么魔法:就是一个有状态管理的异步容器,外加一套回调收集 + 异步调度机制
-
链式调用的本质:每个
.then()返回一个新 Promise,上一个 then 的返回值成为下一个 then 的输入 -
queueMicrotask 的作用:确保 then 的回调总是异步执行,这是 Promise 行为一致性的根基
-
Promise.resolve 的"递归展开":这是 Promise 最难理解的部分——如果 resolve 的是一个 Promise,需要等它完成后再 fulfill 当前 Promise
结语
手写一遍之后,再看 Promise.all、Promise.race、async/await,你会发现它们都是建立在同一套机制上的延伸。
源码不是魔法,原理才是。
完整代码我已经整理到 GitHub,有兴趣的同学可以跑跑测试:
GitHub 地址(可替换为你的仓库)