大家好。我想介绍一下我的项目——Vibe Action。本来可以只讲它做什么、怎么做,但更重要的是回答为什么这个问题——LLM 工具已经数不胜数,而 Vibe Action 并非又一个同类工具。下面我就来解释原因。
先从名字说起。Vibe——这里的含义和许多人想到的不一样。对我来说,一切与 LLM 相关的东西都是 Vibe。凡是亲手写过相机代码、尝试过音视频混流的人——无论是自己写还是通过 LLM 写——都会懂……"vibe coding"这个术语每个人的理解都不一样:对某些人来说,这是“把一切都交给感觉、不看代码”;对另一些人来说,是泛指任何与 LLM 的协作。它没有确切的定义——这反而方便,因为它可以被进一步界定。我提出这样的划分:在vibe coding中,代码由模型来写,而你设定的是感觉(vibe)。在Vibe Action中,行动的是你——你收集上下文、设定步骤、检查结果、做出决策。模型是工具,行动权在你手中。
这个项目的基础不仅是一个想法,而是一种哲学。我积极使用 LLM 已有一年左右,成果非常出色。目前我主要从事系统级开发——将 Kotlin Multiplatform 和 Compose Multiplatform 移植到 Aurora 操作系统上。底层是 Rust,带纯 C-ABI 接口供 Kotlin Cinterop 使用;往上是 Kotlin 库,然后是基于 CMP 的易用库,理想情况下还要移植上游的某个流行库来支持其他目标平台。可想而知,这个技术栈非常庞大。一个任务就是一条链:C++/C/Rust/D-Bus → Aurora Cinterop(Rust)→ Aurora Kinterop(Kotlin)→ 演示应用 → CMP 库 + 上游移植 → 再一个演示应用。也就是说,一个任务涉及一大堆语言和七个针对 Aurora OS(带自定义 Linux target)的项目。
想象一下,你把七个用不同语言写成的庞大项目一股脑塞给代理(agent)——它能把事情全办好,而不会在起步阶段就淹没在上下文里吗?代理在这里行不通。而且问题甚至不在这里:代理会夺走控制权和作者身份,而一旦失去作者身份,就很容易遗漏细节、失去介入代码的机会。我在《代码是谁写的——你还是 AI?》一文中详细分析了这一点——核心只有一个:不能失去对项目的控制。
代理的替代方案是助手(assistant):你坐在驾驶座上,自己收集上下文,检查每一个回答。控制权得以保留。但这种模式也有它的代价:必须手动管理助手——这并不总是方便,尤其是当任务重复出现的时候。
我找到了一种自动化与助手协作的方式——同时不失去控制。Vibe Action 与大多数工具的区别不在于功能特性,而在于其底层的哲学——它的功能只是这种哲学的直接产物。
应用程序
Vibe Action 是一个 CLI 应用程序,其中每条命令都是一个由用户编写的、包含多个 action 的流水线(pipeline)。应用程序会自动加载你的流水线、验证并接入它:只需把文件放进流水线文件夹(~/.vibe-action/actions),或者在配置中添加一个组——可以是你电脑上的目录,也可以是 git 仓库(便于管理大量流水线):
groups:-git:https://github.com/keygenqt/vibe-action-groups.gitpath:/ciname:ciabout:Group of pipelines for CI work.验证是智能的——流水线的变更会被自动获取和检查;还有专门的命令用来重置缓存。
流水线
这是extract——一个工作流水线,用于在日志中查找我们感兴趣的所有内容。日志有时非常庞大,这个任务无论交给助手还是代理,成本都很高。写好一次流水线,我们就永久获得了一个替我们完成这项工作的功能:
# Vibe Action — extract# Extract structured data or matching lines from text and logsversion:0.0.2name:extractabout:Extract structured data or matching lines from text and logsargs:-name:arg_fileshort:'f'input:stringhelp:Path or URL to the log or text fileapi:output:dialoginput:query_promptargs:arg_file:query_file_pathactions:# Step 1 — filter log lines by relevance to the query.-tag:llm_logrun:largeval:-name:'search'data:'query_raw'-name:'val_log'data:'arg_file'mods:'fetch|text'fail:'is:empty:not'each:truereg:'^[^\n]+$'action:|[Task] You are a log filter. Decide if the log line is relevant to the query. The query describes what to find in natural language. If the line is relevant — output the EXACT line character-for-character, preserving original casing. If the line is NOT relevant — output only a single dash: "-" Do NOT skip lines. Process every line. Do NOT modify, normalize, or reformat the line in any way.[Query]{search}[Line]{val_log}# Step 2 — collect the relevant lines back into one block.-tag:out_logrun:valuereg:'^([^\n]+\n)*[^\n]*$'val:-name:'llm_result'data:'llm_log'mods:'split|filter:eq:-|uniq|join'action:'{llm_result}'它的调用方式就像普通的 CLI 命令:
vibe-action data extract "connection refused" -f server.log让我们通过 Vibe Action 所基于的五个原则,逐步拆解这个流水线。
原则一:上下文的控制
上下文对任何 LLM 来说都是最关键的东西。代理会想方设法搜索它需要的数据,把 token 花在遍历项目上——但没有人比你更了解你的项目。人预先知道模型需要什么、不需要什么:他的计划、他的任务、他的上下文。因此在 Vibe Action 中,收集上下文不是碰运气的搜索,而是由你亲手编写的明确步骤。
在extract流水线中,我们传递两个值:
val:-name:'search'data:'query_raw'-name:'val_log'data:'arg_file'mods:'fetch|text'each:true第一个是query_raw:查询的原始文本,即调用示例中的"connection refused"。每条命令都有一个query——就是你紧跟在命令名之后写的内容——而流水线自己声明它需要哪种输入:裸文本(query_raw)、已存在的文件(query_file_path)、项目根目录(query_project_path)、输入的第一行(query_line)。指定了query_file_path——如果路径下确实存在文件,引擎就会把它展开为绝对路径。没有文件——就报错。query可以配置多个候选:第一个期望 URL,第二个期望本地文件,第三个期望剪贴板。来什么,就用什么。
第二个是arg_file:日志文件,由fetch|text操作符下载并准备好传给提示词。不是整个项目,也不是“看看周围”——而恰恰是任务所需的内容。
这就是对模型所获内容的控制——同时也是质量的提升和成本的降低。不过载上下文,就不用为多余的内容付费。任务设定清晰,加上紧凑的上下文——连本地的 3b 模型都能搞定一个步骤。Vibe Action 面向 3b 起步的模型——这不是底线,可以往下继续实验,只是经验之谈:3b 已经非常小了,我建议至少 7b。它们几乎可以跑在任何 PC 上。应用程序的目标之一,就是让弱小的本地模型在流水线上不至于崩掉。
参数each: true是控制的另一半:日志被切成一行一行,每一行都作为一个独立的小请求发给模型。步骤不需要在任何地方“记住”什么,因此它可以被复制扩展——应用程序内置了集群功能:接入多台运行 3b 模型的 PC,一个 action 就可以并行访问所有机器,加速日志的遍历。
测试流水线也变得简单:如果 3b 能胜任任务,任何云端模型只会做得更好——但它对工作而言不再是必需品。
原则二:LLM 是工具
任何工具都有自己的角色。LLM 不应该什么活都干。脚本的行为永远一致,模型则不然。因此,凡是能以确定性方式完成的事情,我们都从模型移到引擎中。
在流水线中,这一点直接体现在 action 的类型上。第一步是run: large,即调用 LLM。而第二步是run: value,其中完全没有模型的参与:
-tag:out_logrun:valueval:-name:'llm_result'data:'llm_log'mods:'split|filter:eq:-|uniq|join'action:'{llm_result}'拆分回答、丢掉短横线、去除重复、重新拼接——CPU 上几毫秒的事,不需要再向模型发一次请求。CPU 处理文本数据的速度比任何 LLM 和 GPU 都快。让 LLM 和 GPU 休息去吧。
这类工作由操作符完成——它们有 30 多个,分为四类:
- Read(世界 → 值)——从外部获取数据:下载 URL、读取文件、从 PDF 提取文本、把代码解析为 AST、截屏。
- Transform(值 → 值)——纯函数:拆分、过滤、排序、拼接、转换大小写。
- Inspect(值 → 是/否)——用于守卫(guard)的谓词,将在下一个原则中介绍。
- Write(值 → 世界)——副作用:写入文件、放入剪贴板。
所有这些都可以通过mods参数在val候选项中使用。此外还有 20 多个内置的system_*类型:时间、架构、CPU 核心数等等——不用run: cmd就能快速从系统中获取数据的捷径。
LLM 在流水线中的角色,仅限于那些离不开智能的部分:判断某一行日志与查询是否相关。而且即便是这部分,它拿到的也是被操作符打磨过的数据——它不需要自己去解读输入,负载降低了,3b–7b 成为完全可行的选项。这就是原则一和原则二的协同工作:人收集了上下文,操作符把它清理干净——留给模型的只有一个小小的任务。
原则三:错误的代价
这里的关键是:人为错误负责。不是“LLM 弄错了”——而是人没有把任务设定准确,或者流水线写得糟糕。责任不会被转嫁给模型——这是作者身份的代价,它很公平。
应用程序提供的工具,不会让错误悄无声息地溜过去。
第一层是每个步骤上的正则表达式。在extract中,模型必须返回与日志行逐字一致的内容(一字不差),或者一个短横线:
reg:'^[^\n]+$'一切不符合模板的内容都无法通过。模型无法“重新发挥”输出结果、无法自行脑补、无法按自己的方式重新排版——正则不会放行。正则表达式是你的知识,在模型回答之前就已写下:你不是怀着“应该没问题吧”的希望去逐条阅读每个回答——而是事先描述了回答应该是什么样子,引擎保证它就是这个样子。
第二层是when和fail守卫。它们是谓词:回答“是”或“否”的检查。流水线中有fail: 'is:empty:not'——检查传给我们的日志是否为空。守卫只能回答问题,不需要修改数据——因此在when和fail中只能使用 Inspect 类谓词操作符。
第三层是引擎本身。对它而言,错误意味着执行崩溃,而不是悄悄继续的理由。正则没通过——报错。参数不匹配——报错。流水线无效——应用程序在加载时就不会接受它。没有任何自动重试,也没有“大概符合模板就算过”的宽松匹配——那些只是让垃圾悄悄混进项目而你毫无察觉的方式。我们控制流水线,自己盯着错误——这是我们的职责,不是模型的。
引擎本身很简单——始终保持在 500 行左右的代码。它以惰性方式、按 action 就绪的顺序执行任务:我们无法在物理上先知道第 1 步的答案再去执行第 2 步——执行图正是按这个逻辑构建的。你看到的是 0.0.2 版本——是的,曾经有 0.0.1,那时的执行图是一次性、完整地构建出来的。但它被重写了:在执行之前预测哪些 action 会参与、哪些不会——这是不可能的,这导致了极其糟糕的补丁式做法。为了让错误排查不变成难题,应用程序提供了丰富的输出和追踪选项。
原则四:透明性
没有“代理自己会搞定”的魔法。流水线是一份一分钟就能通读的文本:你能看到什么发给了模型、什么返回了、每个步骤上设置了哪些检查。它就放在你的文件夹里,在 git 中做版本管理,像普通代码一样进行评审。隐藏的行为比显式的行为更危险——对工具也是如此:事先就知道的限制,永远好过在事故现场才浮现出来的限制。
透明性也延伸到 UI。应用程序有两个插件——分别面向 JetBrains 系 IDE(Android Studio、IDEA Community 等)和 VS Code——它们的行为不藏在 IDE 的设置里。它由你自己声明,就在流水线中的api块里:
api:output:dialoginput:query_promptargs:arg_file:query_file_pathoutput是结果的去向:replace会替换编辑器中选中的文本,clipboard会进入剪贴板,dialog会在窗口中显示。input是向用户询问什么输入,args是插件从哪里获取额外参数:比如arg_file会取自当前在 IDE 中打开的文件。你有了自己编写的流水线——并且由你自己指定插件在与 IDE 协作时应该做什么。不是那种需要另外去别处查证的隐藏机制,而是同一个文件里、紧挨着逻辑的显式声明。
插件本身不需要配置——既不用tasks.json,也不用 External Tools。它从 CLI 获取可用 action 的列表,把编辑器的上下文传给它——选中的文本、文件路径、项目根目录——你直接在 IDE 中拿到结果。
透明性也反向作用于 LLM 自身。编写流水线时需要清楚自己在写什么——vibe-action-groups 仓库中有 AGENTS.md,无论对你还是对模型(当你请它帮忙时),它都能提供帮助。
原则五:发展与退化
最后一条原则没有代码,因为它关乎我们自身。
航空界很早就知道“自动化诱发的技能退化”(automation-induced skill degradation)现象:依赖自动驾驶飞行的飞行员会失去手动驾驶的技能——以至于监管机构强制要求定期进行手动飞行。代码领域发生的事情一模一样。“替你搞定一切”的代理就是自动驾驶:方便,直到它失效的那一天。
Vibe Action 的设计恰恰相反——它不取代你的工作,而是迫使你更有意识地去做这项工作:
- 你自己准备上下文——这意味着你在与代码打交道,把项目装在脑子里。正是这项工作让你不会从项目中掉队。
- 你提前设计检查——正则和守卫迫使你在运行之前、而不是事故之后去思考边界情况。
- 你自己排查错误——引擎不隐藏错误,每一次崩溃都是关于你的任务如何运作的一课。
流水线就是你用代码固化下来的对任务的理解。写完它,你对任务的理解就已经比“让模型做个功能”深刻得多。与此同时,第二个技能也在增长——对 LLM 的驾驭:我们学习与模型沟通并控制结果,而不是把过程交给它。我们不像代理那样走向全面自动化——我们在输入端和输出端控制 LLM,利用它来实现自动化。
由你来决定
当我们选择与 AI 的交互方式时,我们就是在签订一份契约。与代理合作,我们是在自动化自己的工作,交出项目的作者身份,走向一切的全自动。与助手合作,你控制着 LLM——这是另一份契约。与助手协作的目标不是自动化程序员的工作,而是获得最高质量,并对进入项目的一切保持完全控制。
Vibe Action 发展了助手契约,并为其加上了由人控制的自动化。这套哲学植根于项目之中。它经过了实践检验——这是在复杂项目、复杂任务、各种条件下都实际可用的流水线。
如果你认同这套哲学——我想你会喜欢 Vibe Action。LLM 是一件出色的工具,应当加以使用,但重要的是正确地使用。总得有人会驾驭 LLM——也总得有人记得 i32 和 i64 的区别。
🔗 项目官网与文档:https://vibe-action.keygenqt.com