实际使用 AI Agent 时,模型输出长文本是非常常见的问题:解释一个流程能写二十行,对比两个方案能列满一屏。但人眼真正需要的往往是结构,是能一眼看出节点关系、先后顺序和差异点的图形。/show-me 这类 agent skill 正是为这个场景出现的:它以斜杠命令形式加载一套固定的渲染能力,把复杂的结构化信息压缩成紧凑的视觉表示。这篇文章会从 agent skill 的概念讲起,说明它和 MCP 到底有什么区别,再动手实现一个最小可运行的 /show-me 风格渲染技能,最后给出验证、排错和落地建议。
1. 先理解 /show-me 是什么,它解决 Agent 的什么问题
1.1 Agent 输出长文本带来的阅读成本
让 Agent 解释"用户登录后请求如何流转",默认回答往往是:
用户请求先到达 Nginx,Nginx 根据路由规则把请求转发到网关服务,网关完成认证和鉴权后,再把请求分发到用户服务,用户服务查询数据库后返回结果,最后网关把响应汇总并返回给前端。这段描述信息量没问题,但阅读成本很高。你需要先记住 Nginx、网关、用户服务、数据库四个节点,再在脑子里把它们串成一张图。如果流程里有分支、重试、降级逻辑,文本描述会成倍膨胀,很容易漏掉关键依赖。
换成紧凑视觉表示,同样的信息变成下面这样:
┌──────────────────┐ │ Nginx │ └──────────────────┘ │ ▼ ┌──────────────────┐ │ Gateway │ └──────────────────┘ │ ▼ ┌──────────────────┐ │ User Service │ └──────────────────┘ │ ▼ ┌──────────────────┐ │ Database │ └──────────────────┘人眼扫描这张图只需要几秒,节点关系、执行顺序、调用方向全都明确。这正是 /show-me 这类 agent skill 的核心价值:在不依赖图片生成能力的文本通道里,通过固定格式输出,让复杂信息变得可扫读、可定位、可讨论。
1.2 紧凑视觉表示到底包括哪些形式
"紧凑视觉表示"并不是指高大上的图表渲染,而是指在文本通道里用少量字符表达空间关系和结构信息。常见形式包括:
- 使用框线字符绘制的流程图和状态图。
- 用
|和-拼出来的结构表格。 - 用字符长度表示数值大小的条形图。
- 用缩进和分支符号表示的树形依赖关系。
- 用标记符号标注差异点的对比清单。
这些形式非常适合终端、聊天窗口、代码注释和 Markdown 文档。它们不依赖图片渲染服务,也不会因为图片无法加载而丢失信息。更重要的是,它们可以被固定成模板,由代码生成,而不是每次让模型自由发挥。
2. Agent Skill 是什么:它和普通工具调用的边界在哪里
2.1 Skill 的组成与最小文件结构
Agent Skill 可以理解为"给 Agent 准备的可复用技能包"。它不是简单的单一函数,而是一个包含描述文件、脚本、模板、示例的目录。模型在对话中遇到匹配场景时,会把整个技能包加载进来,按里面定义的流程执行。
一个典型的 Skill 目录结构如下:
show-me/ ├── SKILL.md ├── scripts/ │ └── render.py └── examples/ └── flow.jsonSKILL.md 是这个技能包的入口描述,告诉模型"这个技能解决什么问题、什么时候用、怎么用"。scripts 目录存放真正执行逻辑的脚本。examples 目录提供示例输入,帮助模型理解如何把用户意图转成脚本需要的参数。
这种设计思路和普通工具调用有明显差别。普通工具调用是模型在运行期从一组预声明 API 里选择一个函数执行,重点在"调用";Skill 则把一段完整的工作方法打包,包括提示词、命令约定、脚本和边界条件,重点在"复用整套能力"。
2.2 一次 Skill 调用的完整路径
理解 Skill 最好的方式,是看一次完整调用路径。
- 用户在对话里输入触发表达,例如"画一下登录流程"。
- Agent 框架把已注册的 Skill 描述注入上下文,模型根据 SKILL.md 中的 description 判断当前需求是否匹配。
- 匹配后,模型或运行时把用户意图整理成脚本要求的输入结构,例如一个 JSON。
- 脚本在受控环境里执行,返回文本结果。
- Agent 把脚本结果直接返回给用户,而不是重新解释一遍。
最后一步非常关键。如果 Agent 拿到脚本输出的流程图后,因为"想帮忙"而重新表述,表格会被改坏,对齐会被破坏。所以 Skill 描述里通常要明确写一句:脚本输出保持原样,不要二次排版。这也是区分"纯工具调用"和"Skill 工作流"的细节差异。
3. Agent Skill 和 MCP 的区别:一张表说清定位差异
3.1 MCP 解决的是连接标准化问题
MCP,即 Model Context Protocol,是一种标准化的连接协议。它解决的是"模型如何按统一方式访问外部工具和数据源"的问题。一个 MCP Server 通过协议暴露工具、资源和提示,MCP Client 负责连接并管理会话。这样模型就不需要为每个外部系统单独实现一套集成逻辑。
可以这样理解:MCP 关心的是"连到哪里、怎么连、权限怎么控制",而 Skill 关心的是"拿到任务后,按什么流程把活干完"。两者的层次不同,解决的问题也不同。
3.2 一张表看清 Skill 与 MCP 的差异
| 对比维度 | Agent Skill | MCP |
|---|---|---|
| 定位 | 可复用的能力包和执行流程 | 模型与工具/数据源的标准连接协议 |
| 基本组成 | SKILL.md、脚本、模板、示例 | Server、Client、协议定义 |
| 触发方式 | 斜杠命令或场景匹配后加载 | 模型按协议发现并调用工具 |
| 运行依赖 | 通常本地目录即可,依赖轻 | 通常需要启动 Server 进程或远程服务 |
| 权限边界 | 以目录和脚本执行权限为主 | 协议层面的工具发现和授权 |
| 典型场景 | 画流程图、整理表格、批量处理文件 | 查数据库、调外部 API、操作 SaaS 工具 |
| 维护重点 | 提示词质量、脚本稳定性、模板覆盖 | Server 可用性、鉴权、数据格式兼容 |
从这张表可以看出,Skill 偏"经验流程",MCP 偏"连接通道"。Skill 可以完全离线运行,适合把重复性工作固化成稳定产出;MCP 则天然适合连接多个异构系统,解决的是生态互通问题。
3.3 两者不是二选一:组合使用的典型结构
实际项目中,Skill 和 MCP 经常配合出现。一个常见结构是:
用户意图 │ ▼ Agent 框架 │ ├── Skill 层:负责流程编排和渲染 │ │ │ └── /show-me 渲染脚本 │ └── MCP 层:负责数据获取 │ ├── 数据库 MCP Server ├── 监控系统 MCP Server └── 内部 API MCP Server比如用户想把最近一小时的接口错误率画成图。MCP Server 负责从监控系统拉取数据,/show-me 这类渲染 Skill 则负责把数据变成紧凑的文本条形图或表格。Skill 不直接接触数据源,MCP 不直接负责版面呈现,各管一段。
4. 实现一个最小 /show-me 风格渲染 Skill
4.1 目录设计:把渲染逻辑和描述文件分开
下面给出一个思路对齐 /show-me 能力定位的最小实现。你可以把它当成复刻这类技能的起点,实际使用时要根据自己项目的目录和框架版本调整。
先创建目录结构:
mkdir -p show-me/scripts show-me/examples然后把技能描述写入show-me/SKILL.md:
--- name: show-me description: 生成紧凑的文本视觉表示,支持流程和表格两种类型。用户要求画流程图、整理对比表格、梳理结构时使用。 usage: /show-me flow <主题> /show-me table <行列数据> /show-me compare <方案A> <方案B> --- # show-me Skill 当用户需要把复杂信息转成可扫读的视觉结构时,使用本技能。 执行步骤: 1. 把用户意图整理成 JSON 输入。 2. 调用 scripts/render.py,type 指定 flow 或 table。 3. 把脚本输出原样返回,不要重新排版。 注意: - 脚本输出就是最终结果,禁止二次格式化。 - 如果输入信息不足,先向用户确认节点或列名,不要猜测。这里最重要的是description和usage。模型靠 description 判断是否触发技能,靠 usage 学习命令格式。注意:如果 description 写得过于宽泛,模型会在不该触发时误触发;如果写得太狭窄,又会漏触发。
4.2 核心脚本:用 JSON 输入生成紧凑视觉输出
渲染脚本只做一件事:读取结构化 JSON,输出文本图。不要把业务判断放进脚本里,判断交给模型,渲染交给代码。
#!/usr/bin/env python3 """show-me 最小渲染脚本:把 JSON 描述转成紧凑文本图。""" import json import sys from argparse import ArgumentParser def render_flow(nodes): """渲染竖向流程图,节点之间用箭头连接。""" if not nodes: return "(empty flow)" width = max(len(n) + 4 for n in nodes) parts = [] for index, node in enumerate(nodes): top = "┌" + "─" * (width - 2) + "┐" mid = "│ " + node.ljust(width - 4) + " │" bottom = "└" + "─" * (width - 2) + "┘" parts.append("\n".join([top, mid, bottom])) if index < len(nodes) - 1: indent = " " * ((width - 1) // 2) parts.append(indent + "│") parts.append(indent + "▼") return "\n".join(parts) def render_table(headers, rows): """渲染紧凑表格,列宽按内容自适应。""" if not headers or not rows: return "(empty table)" cols = len(headers) widths = [ max(len(headers[i]), max(len(str(r[i])) for r in rows)) + 2 for i in range(cols) ] def fmt(cells): return ( "|" + "|".join( " " + str(cells[i]).ljust(widths[i] - 2) + " " for i in range(cols) ) + "|" ) sep = "|" + "|".join("-" * widths[i] for i in range(cols)) + "|" return "\n".join([fmt(headers), sep] + [fmt(r) for r in rows]) def main(): parser = ArgumentParser(description=__doc__) parser.add_argument( "--type", choices=["flow", "table"], default="flow" ) parser.add_argument("--data", required=True, help="JSON 字符串") args = parser.parse_args() try: data = json.loads(args.data) except json.JSONDecodeError as exc: print(f"JSON 解析失败: {exc}", file=sys.stderr) return 2 if args.type == "flow": print(render_flow(data.get("nodes", []))) else: print(render_table(data.get("headers", []), data.get("rows", []))) return 0 if __name__ == "__main__": sys.exit(main())这段代码实现两个核心函数。render_flow根据节点列表生成竖向流程图,框线宽度由最长节点决定。render_table根据表头和行数据生成 Markdown 风格表格,列宽自动计算。main 函数统一处理 JSON 解析和错误返回,脚本退出码可以用于后续自动化检查。
4.3 接入 Agent 运行环境与本地验证
不同 Agent 框架接入 Skill 的方式不同。常见做法是把show-me目录放到框架指定的 skills 目录,框架启动时扫描注册;也有的框架要求把 SKILL.md 内容维护在系统提示词里,脚本单独存放,模型需要时再触发执行。
如果你使用的框架还不支持目录型 Skill,可以退化成两步:第一步把 SKILL.md 的说明段落放入系统提示词,第二步在模型决定使用渲染时调用render.py。这种方式同样能验证技能逻辑,后续再迁移到完整目录支持即可。
先做本地验证,不依赖 Agent 框架:
python3 scripts/render.py --type flow --data '{"nodes":["request received","validate token","load user","return data"]}'正常输出:
┌──────────────────┐ │ request received │ └──────────────────┘ │ ▼ ┌────────────────┐ │ validate token │ └────────────────┘ │ ▼ ┌────────────┐ │ load user │ └────────────┘ │ ▼ ┌─────────────┐ │ return data │ └─────────────┘再验证表格类型:
python3 scripts/render.py --type table --data '{"headers":["option","latency","cost","risk"],"rows":[["local cache","<1ms","low","consistency"],["redis","1-5ms","mid","ops"],["sql query","5-20ms","high","slow"]]}'正常输出:
| option | latency | cost | risk | |---------------|---------|------|-------------| | local cache | <1ms | low | consistency | | redis | 1-5ms | mid | ops | | sql query | 5-20ms | high | slow |本地验证这一步很重要,它能排除掉 Agent 框架的影响,单独确认脚本本身可用。之后接入框架时,如果出现问题,可以二分定位是脚本问题还是框架调用问题。
5. 运行验证:输入、输出和异常分支都要测
5.1 三组典型测试输入
除了正常输入,还要覆盖边界和异常情况,否则技能在真实对话里会表现在意想不到的地方。
第一类测试是正常场景。比如"画一下用户登录流程",模型应输出flow类型 JSON;"对比三种缓存方案",模型应输出table类型 JSON。这类测试用来确认主流程可用。
第二类测试是边界输入。空节点列表、只有一行数据的表格、列数不匹配的表格,脚本都要有明确表现。示例:
python3 scripts/render.py --type flow --data '{"nodes":[]}'预期输出(empty flow),退出码为 0。这样模型在接到空输入时不会拿到乱码,而是能根据提示决定补充询问。
第三类测试是异常输入。JSON 格式错误、缺少字段、--type传了不支持的值,都应该有非零退出码和 stderr 错误信息。示例:
python3 scripts/render.py --type flow --data 'not-json'预期输出JSON 解析失败: ...,退出码为 2。Agent 框架可以据此判断调用失败,而不是把一段错误文本当图形返回。
5.2 判读输出是否合格的检查清单
技能接入后要建立一套可复用的验收清单:
- 脚本直接执行退出码为 0,stderr 无异常。
- 输出在等宽终端和聊天代码块中都对齐。
- 输出不包含多余反引号、转义反斜杠等会被 Markdown 误解的内容。
- Agent 拿到脚本结果后保持原样返回,没有被重新排版。
- 空输入、错误 JSON、缺少字段时有明确报错,而不是静默失败。
- 中文内容在终端中能正常显示、不乱码。
- 表格列宽按显示宽度计算,中文和英文混排不歪斜。
其中最后两条在中文场景里很容易踩坑。下面的排错章节会详细展开。
6. 常见问题排查:从现象倒推到修复
6.1 输入 /show-me 但没有任何反应
现象:用户在对话里输入/show-me flow ...,Agent 完全忽略,或者回复一段文字而不是图。
可能原因依次检查:
- 技能目录没有被框架扫描到。
- SKILL.md 里的
name或description与触发场景不匹配。 - 模型在系统提示词里看不到技能注册信息。
- 框架日志里根本没有触发记录。
排查方式:先确认技能目录位置和框架约定一致;再直接问模型"你现在能调用哪些技能",看它列出的列表里有没有 show-me;最后查看框架日志里是否存在技能加载失败的记录。注意:不要在 SKILL.md 里写复杂的语法规则,description 越直白,模型越容易准确触发。
6.2 脚本可用,但输出对不齐
现象:单独执行render.py输出正常,通过 Agent 返回后表格错位或框线断裂。
常见原因有三个:模型二次排版破坏了空格;聊天窗口不是等宽字体;中文显示宽度和英文不一致。
模型二次排版是最隐蔽的一个。脚本输出的对齐依赖空格数量,模型只要把某一行的空格重新调整,整张图就废了。所以 SKILL.md 里要写死"保持原样返回"。字体问题建议在技能说明里提示用户切换到等宽字体,或者在生成表格时减少对严格对齐的依赖。第三个问题需要更细致的宽度计算,下面单独说明。
6.3 中文字符和宽度计算问题
现象:中文节点绘图时,框线上下左右对不齐,看起来是"阶梯状"。
原因是 Python 内置的len()函数把中文字符按长度 1 计算,但绝大多数终端把中文按宽度 2 渲染。上面的示例只用了英文节点,所以对齐正常;换成"收到请求"这类中文节点,框线宽度就偏小。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 中文节点框线错位 | 字符显示宽度未按 CJK 计算 | 在终端直接执行脚本观察 | 使用wcwidth计算显示宽度 |
| 中文乱码 | 运行环境编码不是 UTF-8 | 查看输出的字节序列 | 设置PYTHONIOENCODING=utf-8 |
| 列宽计算偏小 | len()不区分字符宽度 | 打印每列计算值 | 宽度计算函数抽象出来单独测试 |
修复方式是在渲染函数里引入wcwidth库,用它替代len()计算宽度。这个改动看起来小,但对中文 Agent 场景是刚需,建议在技能一开始就处理好,而不是等用户反馈后再补。
6.4 和 MCP 工具职责重复
现象:项目里同时有渲染 Skill 和某个 MCP Server,MCP 也能生成图表,两个能力看似重复。
排查时先明确各自边界。如果 MCP Server 提供的是数据查询能力,只是返回了原始数据,那么渲染仍然应该由 Skill 完成;如果 MCP Server 本身就是一个专业的图表生成服务,那么 Skill 可以退化为一个薄封装,负责把用户意图转成 MCP Server 的调用参数。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| Skill 和 MCP 同时被触发 | 两者 description 描述重叠 | 查看调用日志 | 收紧 Skill 的触发条件 |
| MCP 返回数据后 Skill 不渲染 | 编排层未把数据传给脚本 | 检查框架上下文 | 明确数据流方向 |
| 输出格式不一致 | 多套渲染逻辑竞争 | 对比历史输出 | 固定一套渲染器,MCP 只供数据 |
7. 最佳实践与扩展方向:让技能保持干净
7.1 让 Skill 保持"渲染器"的纯粹性
给 Agent 设计技能时,最容易犯的错误是把判断逻辑也塞进脚本里。脚本一旦开始做语义判断,就失去了确定性,也难测试。
推荐边界是:模型负责把开放问题转成结构化输入,代码负责把结构化输入稳定渲染成输出。理解意图是模型的长处,精确排版是代码的长处。让两边各干各的,技能才可靠。
具体落地建议:
- 输入格式固定为 JSON,字段含义写在 SKILL.md 里。
- 渲染脚本不做业务推理,只做格式转换。
- 为每个渲染函数写单元测试,覆盖正常、空输入、异常输入。
- 模板样式集中管理,不要散落在系统提示词里。
7.2 接入生产环境前要补齐的能力
学习环境里跑通脚本,和生产环境接入 Agent 工作流,中间还有一段距离。生产环境至少要考虑以下能力:
| 关注点 | 开发/学习阶段 | 生产环境要求 |
|---|---|---|
| 配置来源 | 脚本参数写死 | 技能目录、JSON schema、风格配置外置 |
| 日志 | 直接打印 | 记录触发时间、输入摘要、退出码、耗时 |
| 权限 | 本机执行 | 限制脚本可访问的目录和环境变量 |
| 回滚 | 直接改文件 | 技能包版本化,可快速切换版本 |
| 测试覆盖 | 手动验证 | 输出快照测试、回归测试 |
| 资源消耗 | 忽略 | 超时、重试、输入长度上限 |
7.3 扩展方向
最小实现跑通后,可以考虑以下扩展:
- 增加更多布局类型:横向流程图、树形结构、环形依赖图。
- 输入格式从 JSON 扩展为 YAML,降低模型生成参数的难度。
- 输出端增加终端配色或 HTML 片段,适用不同展示场景。
- 接入 MCP Server 获取真实数据,再由 Skill 渲染成图,形成完整数据链路。
- 对超大结构做分段渲染,避免一次输出超过聊天窗口限制。
- 增加多语言宽度处理,让中英文混排和纯中文内容都能对齐。
对新手来说,最有价值的练习不是把脚本写得多完善,而是先想清楚一条边界:哪些判断交给模型,哪些判断交给代码。/show-me 这类技能给出了一个很好的答案——把渲染这种确定性工作交给代码,把理解意图这种开放工作留给模型。明白这条边界之后,你就能为自己的 Agent 设计出更多干净的技能包,而不是每次让模型在长文本里自由发挥。