核心结论先行
AI 编码工具(如 Claude Code、Cursor、Devin)通过环境变量向子进程"自报身份",正在形成一种分层的事实约定。消费端工具(如 DuckDB、Vitest)检测到信号后自动切换为机器友好的输出模式,能大幅减少格式噪声与 Token 消耗。
但这远未成为统一标准:
AI_AGENT是一种被多方推广和采纳的通用信号 ,而非权威规范。在实际工程中,检测逻辑呈现为"通用变量 + 工具专用哨兵"的回退链,而生态库也由此分化为专才@vercel/detect-agent与通才std-env两条路线。
前言:当你的 CLI 工具被 AI 调用时,发生了什么?
经常使用 Claude Code、Cursor Agent 或 Windsurf 的开发者一定对这个场景不陌生:AI 在终端里自动跑了一遍测试或数据库查询,结果终端吐出了成百上千行的 ANSI 颜色转义符、动态进度条、ASCII 艺术边框,甚至因为未加 LIMIT 吐出了上万行数据。
这不仅瞬间撑爆了大模型的上下文窗口(Context Window),还会因为格式噪音导致模型解析报错,甚至被静默截断的数据误导。
过去我们区分运行环境,靠的是 process.stdout.isTTY(判断是不是交互式终端)和 process.env.CI(判断是不是持续集成流水线)。但在 AI Coding 时代,第三个核心维度出现了:当前进程是不是正在被 AI Agent 驱动?
如果底层的 CLI 工具能主动感知到"操作者是 AI",它能做出什么改变?各大工具又是如何约定这个"暗号"的?今天我们就来拆解这一正在成型的工程实践。
一、 消费端实践:DuckDB 与 Vitest 的"AI 专属模式"
在探讨底层怎么检测之前,我们先看看顶层工具检测到 AI Agent 后,究竟能玩出什么花样。最典型的代表是数据分析领域的 DuckDB 和前端测试领域的 Vitest。
1. DuckDB CLI:为大模型量身定制的 Agent Mode
DuckDB 2.0 为 CLI 引入了专门的 Agent Mode 。有意思的是,它的触发并不是简单地看一眼环境变量,而是一个严谨的复合条件:
- 环境变量命中 :检测到已知的 AI Agent 环境变量(如
AI_AGENT); - 非交互式输出:标准输出不是终端(即非 TTY,例如输出被管道接到了 Agent 的宿主进程);
- 尊重显式意图 :命令行没有显式传入输出格式参数(如
-csv、-json等)。
只有当这三个条件同时满足时,Agent Mode 才会自动激活(当然,开发者也可以通过 -agent / -no-agent 参数或 DUCKDB_AGENT_MODE 环境变量强制接管,且命令行参数优先级最高)。
激活后,DuckDB 的输出并非只是简单地"关掉加载动画",而是进行了格式层面的系统性重塑:
- 紧凑型 Markdown 表格 :去除人类视觉对齐所需的空格填充(Padding),将列类型直接写在表头单元格中。仅这一项改动,典型结果就比原生的
duckbox格式减少了 25%--65% 的字节数。 - 显式截断标记(防幻觉关键) :当结果超过 1000 行或 10000 字节时,仅渲染首尾各 20 行,并在中间插入极其明确的文本标记说明省略了多少行。这直接避免了大模型因为"静默截断"而产生数据已完整的幻觉。
- 结构化报错(JSON to stderr) :错误信息直接以 JSON 格式输出到
stderr,让模型可以直接按 Schema 提取关键信息,而不是在一整屏人类可读的堆栈文本里大海捞针。 - 紧凑 EXPLAIN 与熔断机制 :大查询的计划估计和进度输出到
stderr,一旦超出阈值提前停止并报告下界。
理性看待 Token 节省数据
DuckDB 官方基准测试显示,开启 Agent Mode 后,模型读取的查询输出从 123.6k tokens 降至 50.8k,降幅高达 59%,且所有测试均产出了正确答案。
但我们也需要保持工程上的清醒:同一批分析指出,这 59% 的节省仅占每轮对话总输入的 0.5% 左右 ------因为长长的 System Prompt、工具定义和历史代码上下文占据了绝大部分开销,总运行时间和账单成本并不会同比例暴跌。Agent Mode 真正的核心价值在于"提升输出的可解析性"与"消除静默截断带来的推理错误",而非单纯省钱。
2. Vitest:从输出精简到运行时决策
相比于 DuckDB 的重度改造,前端工具链中的 Vitest 走的是轻巧路线。
当你在配置中未显式指定 reporters 时:
- 在普通终端运行,默认使用带动态渲染的
defaultreporter; - 一旦检测到 AI 编码代理,自动切换为
minimalreporter,剔除多余的动态刷屏,只输出核心结果。
如果你显式配置了 reporters,默认列表会被覆盖。如果你想在保留这套智能默认行为的基础上追加自定义 reporter,可以在 vitest.config.ts 中展开默认配置:
typescript
import { configDefaults, defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// configDefaults.reporters 内部已根据是否处于 Agent 环境动态赋值
reporters: [...configDefaults.reporters, 'my-custom-reporter'],
},
})
更值得关注的是,翻看 Vitest 的源码可以发现,它在 configDefaults 中直接读取了 stdEnv.isAgent,并将其用于 Watch 模式的启停判断逻辑 (例如在 Agent 环境下默认关闭阻塞式的监听模式),而不仅仅局限于 TTY 检测。这说明 Agent 检测已经深入到了测试框架的运行时行为决策,而不再只是输出层的"皮肤切换"。
二、 幕后暗号:AI_AGENT 通用约定与工具特定哨兵
消费端工具表现得如此智能,那它们到底是怎么认出调用者是 AI 的?
在 DuckDB 引入 Agent Mode 的 PR 中,作者留下了一句非常诚实的自述:
"There is no single convention yet."(目前还没有单一的标准约定。)
因此,目前工业界实际的检测模型并不是检查某一个标准变量,而是一条分层回退链(Fallback Chain):
第一层:通用自声明(AI_AGENT / AGENT)
如果环境中设置了 AI_AGENT 或 AGENT,检测逻辑会直接采信其值作为当前 Agent 的身份(或者至少确认"当前处于 Agent 环境")。目前包括 Claude Code、Goose、Amp 在内的多个工具都已经主动注入了该变量。
第二层:工具特定哨兵(Tool-specific Sentinels)
当通用变量缺失时,检测逻辑会回退去检查各个工具私有的环境变量。例如:
CLAUDECODE(Claude Code)CURSOR_AGENT(Cursor)CODEX_*(OpenAI Codex CLI)GEMINI_CLI(Google Gemini CLI)COPILOT_*(GitHub Copilot)
Hugging Face 的开源检测代码也验证了这一模式:他们按照工具的市场普及度(Prevalence)对这些特定标记进行了排序,并在源码注释中明确标注------将 AI_AGENT 和 AGENT 视为通用标准(任何工具均可设置),其余检查则作为工具特定的兜底。
案例拆解:为什么 Claude Code 要设两个变量?
如果你在 Claude Code 的终端里打印环境变量,你会发现它既触发了通用信号,又设置了专属的 CLAUDECODE=1。为什么要多此一举?
因为 CLAUDECODE=1 这个工具特定哨兵承担了双重职责:
- 对外自报家门:让子进程(如你的构建脚本、测试框架)识别出自己正运行在 Claude Code 内部。
- 对内防嵌套守卫(Anti-nesting Guard) :Claude Code 启动的子进程会继承这个环境变量。如果子进程(或 AI 自己写出的脚本)试图在内部再次拉起一个新的 Claude Code 实例,新实例检测到
CLAUDECODE=1就会立刻拒绝启动,并抛出经典报错:
Claude Code cannot be launched inside another Claude Code session
因此,我们可以对当前的生态现状做一个准确的定性:AI_AGENT 是一个被上游检测库积极推广、被多个下游消费端工具采纳的"正在形成中的事实约定",而非某家权威机构颁布的强制标准。 它在检测优先级上排在最前,但覆盖率取决于各家 Agent 厂商的跟进程度。
三、 检测库选型:专才 @vercel/detect-agent vs 通才 std-env
既然要写回退链,我们显然不应该在每个业务项目里自己维护一堆 if (process.env.XXX)。目前开源社区已经沉淀出了两个主流的检测库,且恰好代表了两种截然不同的设计哲学。
路线一:专才 @vercel/detect-agent
Vercel 推出的这个库目标极其纯粹:尽可能广泛、准确地识别市面上所有的 AI 编码环境,并积极推动 AI_AGENT 成为行业通用规范。
typescript
import { detectAgent } from '@vercel/detect-agent'
const { isAgent, agent } = await detectAgent()
if (isAgent) {
console.log(`当前运行在 AI Agent 环境中: ${agent?.name}`)
}
它的核心架构亮点在于数据驱动的规则注册表:
- 配置与逻辑解耦 :所有的检测规则都定义在仓库的
agents.json中,按数组顺序求值,首个匹配胜出(First-match wins)。因此,更具体、特征更强的规则会被排在更通用的规则之前。 AI_AGENT最高优 :AI_AGENT被置于规则表的最顶端。只要该变量存在,其值会直接作为 Agent 名称返回,直接跳过后续冗长的工具特征匹配。- 推广命名规范 :它在 README 中提出了一套推荐的命名约定------小写字母、连字符分隔,可选
@加版本号 (例如claude-code、cursor-cli、devin@1、custom-agent@2.0)。注意,这目前仍属于社区倡议,尚未被所有工具严格遵守。 - 跨语言复用 :官方直接提供了 JS/TS 和 Go 两个版本;由于核心规则抽离成了纯 JSON(
agents.json),其他语言生态(如 Hugging Face 的 Python 实现)也在不同程度地借鉴这套注册表设计。
路线二:通才 std-env
来自 UnJS 生态的 std-env(也是 Nuxt、Vitest 等众多知名框架的底层依赖)定位则是"运行时无关的通用环境检测工具箱"。
它同时覆盖了 JavaScript 运行时(Node / Deno / Bun / workerd)、CI 提供商(GitHub Actions / GitLab CI 等)以及 AI 编码代理。Agent 检测只是它的一个子模块:
typescript
import { isAgent, agent, agentInfo } from 'std-env'
if (isAgent) {
// 例如在 Cursor 中输出: "cursor"
console.log(`Detected AI Agent: ${agent}`)
}
它的实现特点是代码内聚:
- 检测逻辑直接写在源码(
src/agents.ts)中,维护了一个精选的主流支持列表(cursor、claude、devin、replit、gemini、codex 等),同样支持通过AI_AGENT显式指定名称。 - 与
@vercel/detect-agent相比,它的 Agent 覆盖列表相对精简,新增规则的发布也绑定在整个库的发版节奏上。 - 最大优势是"零额外依赖成本" :如果你的项目本身就需要判断
isCI、isLinux、nodeVersion或hasTTY,直接从std-env解构出isAgent即可,完全不需要为了检测 AI 单独引入一个新包。
核心维度横向对比
为了方便大家在技术选型时快速拍板,这里整理了一份对比表:
| 对比维度 | @vercel/detect-agent(专才) |
std-env(通才) |
|---|---|---|
| 核心定位 | 专项 Agent 检测库 | 通用环境检测工具箱(Agent 仅为子模块) |
| 覆盖广度 | 极广,紧跟社区长尾新工具,持续高频更新 | 覆盖主流头部 Agent(Cursor/Claude/Codex 等),相对精简 |
| 规则实现机制 | 数据驱动 (基于 agents.json,首个匹配胜出) |
代码驱动 (硬编码于 src/agents.ts) |
对 AI_AGENT 的态度 |
标准制定与积极推广者(定义了推荐命名格式) | 兼容支持者 (支持读取 AI_AGENT 显式覆盖) |
| 多语言生态 | 原生提供 JS/TS + Go,JSON 规则易于跨语言移植 | 专注于 JS/TS 生态 |
| 最佳适用场景 | 核心业务强依赖精准识别各类细分 Agent 身份 | 工程中已有/需要综合判断 CI、OS、Runtime 等多种环境 |
四、 架构与工程落地指南
如果你正在开发一款内部 CLI 脚手架、构建工具、测试框架,或者任何可能被开发者在 AI 终端里调用的 Node.js 脚本,基于上述行业现状,以下有四条直接可用的落地建议:
1. 自研检测逻辑:坚持"通用优先,哨兵兜底"
如果你不想引入外部依赖,只是想在自己的脚本里写一个轻量判断函数,请优先检测 AI_AGENT(以及 AGENT),但绝不要假设它一定存在。参考 DuckDB 和 Hugging Face 的分层策略:
typescript
export function isRunningInAiAgent(): boolean {
const env = process.env
// 1. 最高优先级:通用自声明约定
if (env.AI_AGENT || env.AGENT) {
return true
}
// 2. 回退链:主流工具特定哨兵变量
const toolSpecificSentinels = [
'CLAUDECODE',
'CURSOR_AGENT',
'GEMINI_CLI',
'CODEX_ENV',
'COPILOT_AGENT'
]
return toolSpecificSentinels.some((key) => Boolean(env[key]))
}
2. 依赖选型:按需对号入座
- 如果 Agent 检测是你的核心竞争力 (比如你在做一款专门针对不同 AI 编码工具做遥测分析、Prompt 注入适配或差异化计费的 SDK),请毫不犹豫地选择
@vercel/detect-agent。它的数据驱动注册表(agents.json)在规则维护、覆盖广度和跨语言对齐上优势明显。 - 如果 Agent 检测只是 CLI 众多环境判断中的一环 (比如你只是想在 AI 调用时关掉
ora旋转动画、把日志切换成极简模式),直接选std-env。你大概率已经用到了它的isCI或hasTTY,复用同一个轻量依赖更加经济,Vitest 的生产实践已经替你踩平了这条路。
3. 交互设计:参考 DuckDB 的"复合触发条件"
不要一检测到 isAgent === true 就粗暴地锁死输出格式。优秀的 CLI 工具应当像 DuckDB 一样具备层次感:
- 最高优 :用户显式传入的命令行参数(如
--json、--no-agent); - 次高优 :结合
isAgent与!process.stdout.isTTY自动触发极简/结构化模式; - 重点改造项 :去掉無意义的进度条与颜色控制符、将冗长报错结构化、对超长输出进行截断并打印显式的截断提示(如
[... 省略 850 行,请使用 --limit 查看更多 ...])。
4. 对 Token 节省保持理性预期
在向团队或老板汇报"为 CLI 增加 AI Agent 模式"的收益时,不要过度吹嘘"能帮公司省下一半的大模型 API 账单"。正如前文 DuckDB 的基准数据所示,单次工具输出省下的 59% Token,在动辄几万 Token 的 System Prompt 和多轮代码上下文面前占比极小。
这项改造真正的工程价值在于「鲁棒性」 :通过提供高信噪比、结构化、截断透明的输出,让 AI Agent 少走弯路、少犯低级解析错误、减少因报错重试导致的额外对话轮次------减少无效重试轮次,才是真正降低推理耗时与成本的关键。