一个 Agent 如果只是默默干活、最后丢个结果出来------你永远不知道它中间在想什么、做了几步、有没有卡住。
一、先看效果
同样一个任务"创建 React 项目",没做可视化之前:
css
$ node src/main.js
(终端空白 30 秒...)
任务完成,项目已创建在 react-todo-app
做了可视化之后:
bash
$ node src/main.js "创建一个 todo 应用"
🚀 Agent 启动
📝 任务: 创建一个 todo 应用
━━━ 第 1/15 轮 (累计 0.0s) ━━━
📋 模型请求调用 2 个工具:
→ execute_command
command: pnpm create vite react-todo-app --template react-ts
⚡ execute_command (3200ms)
✔ execute_command → 命令执行成功
━━━ 第 2/15 轮 (累计 5.2s) ━━━
📋 模型请求调用 1 个工具:
→ write_file
filePath: react-todo-app/src/App.tsx
⚡ write_file (45ms)
✔ write_file → 3847 字符
━━━ 第 3/15 轮 (累计 8.1s) ━━━
🤖 Agent 最终回复:
TodoList 应用已创建完成,功能包含添加、删除、筛选和本地存储。
✅ Agent 执行完成!
同样的功能,后者让你看清楚 Agent 的每一步决策。
这篇文章讲的就是怎么做到这一点。
二、Chalk 是什么
一个给终端文字上色的库,只有一件事:把 console.log 变成彩色的。
js
import chalk from 'chalk'
console.log(chalk.green('成功')); // 绿色文字
console.log(chalk.red.bold('错误')); // 红色加粗
console.log(chalk.blue('Agent 回复')); // 蓝色
console.log(chalk.gray('辅助信息')); // 灰色(不抢眼)
2.1 Chalk v5 的 ESM 变化
v5 是 ESM-only,只能用 import:
js
// ✅ v5 正确写法
import chalk from 'chalk';
chalk.green('hello');
// ❌ 常见错误 --- 从旧版本教程抄来的
const chalk = require('chalk'); // v5 不支持 require
import { Chalk } from 'chalk'; // Chalk 是类,不是你要用的
2.2 常用颜色与使用场景
js
chalk.green('✔ 成功') // 工具执行成功
chalk.red('✖ 失败') // 错误信息
chalk.yellow('⚠ 警告') // 达到最大迭代次数
chalk.cyan('→ 工具调用') // Agent 计划做什么
chalk.magenta.bold('分隔线') // 轮次分隔
chalk.blue('模型回复') // Agent 的最终输出
chalk.gray('辅助信息') // 文件路径、时长、ID
chalk.white('命令内容') // 具体要执行的命令
三、从散弹到统一:Logger 模式的设计
3.1 第一版:各自为政
最早我在四个工具文件里各自写 console.log:
js
// readFileTool.js
console.log(`[工具调用] read_file: (${filePath}) 成功读取${content.length}字节`);
// writeFileTool.js
console.log(`[工具调用] write_file: (${filePath}) 创建成功写入${content.length}字节`);
// execCommandTool.js
console.log(`[工具调用] execute_command(${command}) 成功执行`);
三个问题:
- 格式不统一 :有的带
[工具调用]前缀,有的没有;有的显示字节数,有的不显示 - 没有颜色:全是白字,关键信息淹没在日志里
- 职责混乱:工具既要执行逻辑又要管展示
3.2 第二版:加 Chalk,但没统一
改成彩色了,但日志逻辑仍然散落在各个文件:
js
// 每个工具文件都 import chalk from 'chalk',然后各自 console.log
改一个颜色规范要动四个文件。新增工具还得记住用什么格式。
3.3 最终版:Logger 集中管理
创建 src/utils/logger.js,所有可视化逻辑集中在一处:
js
import chalk from 'chalk';
export const logger = {
phase(msg) {
console.log(chalk.magenta.bold(`\n━━━ ${msg} ━━━`));
},
toolPlan(toolCalls) {
console.log(chalk.cyan.bold(`\n📋 模型请求调用 ${toolCalls.length} 个工具:`));
for (const tc of toolCalls) {
console.log(chalk.cyan(` → ${tc.name}`));
if (tc.args) {
for (const [key, value] of Object.entries(tc.args)) {
console.log(chalk.gray(` ${key}: ${value}`));
}
}
}
},
toolSuccess(name, duration, summary) {
console.log(chalk.green(` ✔ ${name}`) +
chalk.gray(` (${duration}ms)`) +
(summary ? chalk.green(` → ${summary}`) : ''));
},
toolFail(name, duration, reason) {
console.log(chalk.red(` ✖ ${name}`) +
chalk.gray(` (${duration}ms)`) +
chalk.red(` → ${reason}`));
},
agentReply(content) {
console.log(chalk.blue.bold('\n🤖 Agent 最终回复:'));
console.log(chalk.blue(content));
},
// ... 更多方法
};
现在工具文件里一行 console.log 都没有------纯函数,只返回结果字符串。所有可视化由 ReactAgent 调用 logger 完成。
四、可视化与执行分离
4.1 改造前的工具
js
// 旧版 writeFileTool ------ 自己打日志
async ({ filePath, content }) => {
try {
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, content, 'utf8');
console.log(chalk.green(`✔ write_file ${filePath} (${content.length} 字节)`));
return `成功写入${filePath}`;
} catch (err) {
console.log(chalk.red(`✖ write_file ${filePath} 失败: ${err.message}`));
return `写入失败: ${err.message}`;
}
}
4.2 改造后的工具
js
// 新版 writeFileTool ------ 纯函数,只返回结果
async ({ filePath, content }) => {
try {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, content, 'utf8');
return `成功写入${filePath} (${content.length} 字节)`;
} catch (err) {
return `写入文件 ${filePath} 失败: ${err.message}`;
}
}
4.3 Agent 层统一调用 logger
js
// ReactAgent.js ------ 统一管理可视化
for (const toolCall of response.tool_calls) {
logger.toolStart(toolCall.name, toolCall.args); // 开始执行
const toolStart = Date.now();
let toolResult;
try {
toolResult = await tool.invoke(toolCall.args);
} catch (err) {
toolResult = `工具执行出错: ${err.message}`;
}
const duration = Date.now() - toolStart;
logger.toolSuccess(toolCall.name, duration, this._summarizeResult(toolCall.name, toolResult));
}
4.4 好处
| 旧方案 | 新方案 | |
|---|---|---|
| 工具文件 | 含 chalk + console.log | 纯函数,只 return |
| 颜色规范 | 散落各处,容易不一致 | logger.js 一处定义 |
| 新增工具 | 要记住日志格式 | 写工具逻辑即可,logger 自动处理 |
| 修改展示 | 改 N 个文件 | 只改 logger.js |
| 测试 | 日志干扰测试 | 工具是纯函数,好测 |
五、让信息有层次
好的可视化不是"所有东西都亮",而是关键信息跳出来,辅助信息退到背景里。
5.1 颜色语义体系
🟢 green --- 成功(工具执行完毕、Agent 完成)
🔴 red --- 失败(错误、异常)
🔵 blue --- Agent 回复(模型的最终输出)
🟣 magenta --- 阶段标记(轮次分隔线)
🟡 cyan --- 计划/意图(模型打算调用哪些工具)
🟡 yellow --- 警告(达到上限、命令正在执行)
⚪ gray --- 元数据(文件路径、耗时、ID)
5.2 控制信息密度
长字符串自动截断:
js
// logger.js 中的处理
const display = typeof value === 'string' && value.length > 80
? value.slice(0, 80) + '...'
: value;
文件内容可能有几千行,不能在日志里全打印。只显示摘要(多少字符 / 多少项 / 命令退出码)。
5.3 计时信息
每轮迭代和每个工具调用都有计时:
scss
━━━ 第 3/15 轮 (累计 8.1s) ━━━ ← 总耗时
✔ execute_command (3200ms) ← 单步耗时
✔ write_file (45ms)
这让用户能直观感受到:哪些步骤慢、慢在哪里。
六、总结
这篇文章讲了三个层次的事:
第一层 --- 工具 :Chalk v5 是 ESM-only,import chalk from 'chalk' 即可,支持链式调用 .red.bold。
第二层 --- 模式 :logger.js 集中管理模式,工具文件纯净化,Agent 层统一调度。
第三层 --- 思想 :可视化不是为了花哨,是为了可观测性。Agent 是一个黑盒------它想什么、做什么、是否卡住,用户完全不知道。好的日志输出让黑盒变得透明。
三篇文章构成了一个完整的学习路径:
| 篇 | 主题 | 核心问题 |
|---|---|---|
| 第一篇 | Agent 设计 | Agent 为什么能工作?(ReAct 循环) |
| 第二篇 | 内置模块 | Agent 怎么干活?(fs / path / spawn / readline) |
| 第三篇 | 可视化 | Agent 干了什么?(chalk / logger 模式) |
仓库地址 :min-cursor