
最近在给 Agent 做“可视化能力”的时候看到 Show HN 上有人开源了一个很有意思的技能/show-me。它的目标非常纯粹——当用户说“画一下流程”“看一下模块关系”“展示一下架构”的时候不要让 Agent 输出一段又长又难读的文字而是直接生成一份紧凑的视觉表示。这篇文章会围绕/show-me展开讲清楚三件事Agent Skill 到底是什么、它和 MCP 有什么区别、以及如何自己动手实现一个类似的可视化技能。文章里会包含完整可运行的代码示例、目录结构、运行结果和排错思路。无论你是刚接触 LLM 应用开发的新手还是已经在做 Agent 平台的后端工程师都应该能从中拿到可以直接落地的内容。1. 背景/show-me 是什么为什么需要它1.1 从一次对话场景说起假设你正在使用一个接入大模型的 Agent你输入帮我画一下订单系统的处理流程从下单到支付再到发货。如果没有可视化技能Agent 很可能会给你一段这样的回复1. 用户下单 2. 系统创建订单 3. 用户发起支付 4. 支付回调确认 5. 仓库发货 6. 订单完成这段文字信息没有丢失但阅读体验很差流程之间的关系、分支、层级都不直观。如果 Agent 能输出一张紧凑流程图效果会好很多。/show-me这类技能要解决的就是这个问题——把“让模型想起来去画图”变成“模型有标准方法去画图”。1.2 /show-me 的定位从项目命名来看/show-me采用了“斜杠命令”的形式。在不少 Agent 产品中斜杠命令是触发技能的一种交互方式类似聊天软件里的/help、/clear。用户在输入框里敲/show-me xxxAgent 就会路由到对应的技能模块执行可视化逻辑。这种设计的好处是触发意图明确不需要模型猜测用户是不是想画图。技能内部可以预置格式、模板、渲染规则保证输出一致。斜杠命令天然适合“工具型”技能和自然语言对话互补。1.3 什么是 Compact Visual Representations“Compact Visual Representations”可以翻译为“紧凑视觉表示”。它的核心思想是用尽可能少的 token、尽可能小的体积把结构化信息可视化。常见形式包括ASCII 流程图用-、|、等字符表达流程。Unicode 盒式框图使用┌ ─ ┐ │ └ ┘等制表符画框。轻量 SVG输出一段极简 SVG 代码嵌入 Markdown 后直接显示。文本树结构用缩进和符号表达层级关系。为什么要强调“紧凑”因为在 LLM 应用里输出长度直接关联 token 成本、响应速度和上下文占用。一次对话如果输出几千字符的 SVG很快会把上下文窗口塞满而一段精心设计的 ASCII 图可能只占几十个 token信息量却差不多。┌──────────────┐ │ 用户请求 │ └──────────────┘ │ ▼ ┌──────────────┐ │ 技能路由 │ └──────────────┘这就是一个典型的紧凑视觉表示可以直接在终端阅读也可以嵌入文档体积小、含义清楚。2. Agent Skill 与 MCP 到底有什么区别“Agent Skill”是最近非常热的概念很多读者会把它和 MCP 混在一起。这里先把两者的定位梳理清楚。2.1 先理解 Agent SkillAgent Skill 可以理解为一个“技能包”。它通常包含一段说明文档例如SKILL.md描述技能是干什么的、什么时候用。一些使用示例告诉模型应该怎么调用。可选的脚本或模板文件例如 Python 脚本、JSON 模板。技能的本质是“教 Agent 如何完成某一类任务”。它强调过程、步骤和规则。比如show_me技能会告诉模型当用户要求可视化时先确认输出格式再把数据转成统一结构最后调用渲染器。技能被触发后通常会把说明文档注入到模型的上下文里模型按照说明一步步执行。因此技能更像一份“操作手册 工具包”。2.2 再理解 MCPMCPModel Context Protocol模型上下文协议是一套开放标准用来统一“模型如何调用外部工具和数据源”。它的定位是协议层解决的是连接问题。一个 MCP 系统包含MCP Host模型所在的应用程序。MCP Client负责与 Server 通信的客户端。MCP Server暴露工具、资源和提示词的服务器。MCP 定义了三类核心原语Tools模型可以调用的函数。Resources可以被读取的数据。Prompts可复用的提示模板。通信基于 JSON-RPC 2.0传输方式可以是标准输入输出stdio也可以是流式 HTTP。MCP 最大的价值在于标准化同一个 MCP Server 可以被不同的 Agent 应用复用不需要为每家厂商单独适配。2.3 两者的对比对比维度Agent SkillMCP本质定位技能包、操作手册通信协议、连接标准关注重点教 Agent 如何完成任务定义工具如何被外部调用典型组成SKILL.md 脚本 示例Server Client Tools/Resources触发方式指令匹配、意图识别模型按工具定义发起调用标准化程度各家实现各有差异统一规范跨平台复用粒度任务级偏完整流程原子能力级偏单一工具示例可视化技能、代码审查技能数据库查询工具、天气 API 工具需要特别强调的是它们不是竞争关系而是互补关系。一个 Skill 内部完全可以调用多个 MCP 工具反过来一个 MCP 工具也可以被某个 Skill 编排成更复杂的流程。举个简单例子show_me技能为了获取真实的系统拓扑数据可能会通过 MCP 去调用一个监控系统的 Server拿到节点信息后再执行渲染逻辑。Skill 负责“怎么画”MCP 负责“数据从哪来”。3. 环境准备与目录结构3.1 基础环境说明实现一个/show-me技能并不需要复杂环境。本文示例只使用 Python 标准库不依赖第三方包因此对 Python 版本要求很宽松3.9 以上基本都能运行。如果你本机已经有 Python 环境可以直接复制代码运行。需要的工具Python 3.9。一个命令行终端Windows CMD、PowerShell 或 macOS/Linux Terminal 均可。一个支持等宽字体的编辑器VS Code、Sublime 等。如果你使用的是某个现成 Agent 框架例如 Claude Agent Skills 或其他自研框架注册方式可能略有不同但核心思路一致把技能目录放到 Agent 能扫描到的位置然后在技能描述里写清楚触发条件和输出格式。3.2 示例项目结构为了便于理解本文设计一个极简的 Agent 项目目录结构如下my-agent/ ├── agent.py ├── skills/ │ └── show_me/ │ ├── SKILL.md │ ├── renderer.py │ └── examples/ │ ├── ascii_example.txt │ └── svg_example.svg各文件职责agent.py极简 Agent 入口负责技能注册和路由。skills/show_me/SKILL.md技能元信息和说明文档。skills/show_me/renderer.py核心渲染器负责把结构化数据转成紧凑视觉表示。examples/输出示例目录。这个结构刻意保持简单方便你快速理解。真实项目里还会增加测试、日志、配置管理等内容但核心模式是一样的。4. 核心原理紧凑可视化应该如何设计4.1 为什么要在意“紧凑”在很多 Agent 应用里输出即成本。一张位图图片无法直接嵌入文本上下文而 SVG、ASCII、文本树都只是普通字符串可以进入上下文、可以被 diff、可以被版本管理。紧凑表示还有另一个好处利于模型迭代。当图表以文本形式返回时模型可以继续对它做修改。比如你说“把第三个节点删掉”模型只需要编辑文字而不是重新生成图片。这种“可编程的可视化”在工程场景中价值极大。4.2 常见紧凑可视化格式对比格式token 消耗可读性适用场景ASCII 箭头低中快速流程、聊天窗口Unicode 盒式图中高架构图、分层图极简 SVG中高高文档嵌入、演示文本树低中目录、层级关系Mermaid 语法中中需要渲染引擎的文档这里需要根据场景选择。如果 Agent 的回复直接展示在终端ASCII 和 Unicode 盒式图最合适如果要嵌入在线文档或者被浏览器渲染SVG 更稳定如果团队已经接了图表渲染服务也可以输出相应语法文本。4.3 设计原则设计一个可视化技能时建议遵循以下原则统一中间格式。无论用户输入是自然语言、JSON 还是简单文本先解析成统一结构再交给渲染器。本文采用nodes edges结构。输出可降级。渲染 SVG 失败时要能回退到 ASCII。限制规模。超过 20 个节点建议分组或拆成多张图避免输出过长。优先等宽字体。盒式图依赖对齐非等宽字体会导致错位。把格式选择权留给用户。默认输出最省 token 的格式但允许用户显式指定。5. 完整实战实现一个 /show-me 技能下面进入正题。我们会实现一个可运行的/show-me技能支持三种输出格式ascii、box、svg。5.1 创建技能目录首先创建项目目录mkdir -p my-agent/skills/show_me/examples cd my-agent后续所有文件都放在这个目录下。5.2 编写 SKILL.md技能说明文档是 Agent Skill 的关键组成部分。它告诉 Agent 这个技能什么时候该用、怎么用。文件路径为skills/show_me/SKILL.md--- name: show_me description: 当用户需要查看架构、流程、依赖关系或提出“画一下”“可视化”时使用本技能生成紧凑视觉表示。 version: 1.0.0 --- # show_me 把结构化数据或简单文本描述渲染成紧凑的视觉表示。 ## 能力边界 - 输入JSONnodes edges或 A - B 简单文本。 - 输出格式ascii、box、svg。 - 不生成位图不处理超过 20 个节点的超大图。 ## 使用方式 1. 确认用户期望的输出格式。 2. 把描述转成结构化 JSON。 3. 执行 python renderer.py -f format -d json。 4. 将输出直接返回给用户。 ## 示例 见 examples/ 目录。注意SKILL.md头部使用 YAML 格式的元信息description字段非常重要它决定了 Agent 在什么场景下会选中这个技能。描述越具体路由准确率越高。5.3 实现核心渲染器核心渲染器是技能的“引擎”。文件路径为skills/show_me/renderer.py# 文件路径skills/show_me/renderer.py show_me 技能的核心渲染器。 把结构化的图数据渲染成紧凑的视觉表示支持三种格式 - ascii普通文本箭头流程图token 消耗最少 - boxUnicode 盒式框图适合架构图和分层图 - svg极简 SVG适合直接嵌入 Markdown 文档 示例 python renderer.py -f ascii -d 登录 - 鉴权 - 查询 - 返回 python renderer.py -f box -d 登录 - 鉴权 - 查询 - 返回 python renderer.py -f svg -d {nodes:[{id:a,label:A}],edges:[]} from __future__ import annotations import argparse import json from typing import Dict, List # ---------- 1. 输入解析 ---------- def parse_input(raw: str) - Dict: 把用户输入解析成统一的 nodes edges 结构。 raw (raw or ).strip() if not raw: return {nodes: [], edges: []} if raw.startswith({): data json.loads(raw) return normalize(data) # 支持 A - B; B - C 这样的简单文本 return parse_text(raw) def normalize(data: Dict) - Dict: nodes data.get(nodes) or [] edges data.get(edges) or [] return {nodes: nodes, edges: edges} def parse_text(raw: str) - Dict: nodes: List[Dict] [] edges: List[Dict] [] seen: Dict[str, str] {} def _get_id(label: str) - str: if label not in seen: seen[label] fn{len(seen) 1} nodes.append({id: seen[label], label: label}) return seen[label] for part in raw.replace(, ;).replace(, ;).split(;): part part.strip() if not part: continue if - in part: left, right part.split(-, 1) left, right left.strip(), right.strip() _get_id(left) _get_id(right) edges.append({from: seen[left], to: seen[right], label: }) else: _get_id(part) return {nodes: nodes, edges: edges} # ---------- 2. 渲染器 ---------- def render_ascii(data: Dict) - str: 最紧凑的箭头流程表示token 消耗最少。 lines [] for node in data.get(nodes, []): lines.append(f[{node.get(id, ?)}] {node.get(label, )}) if data.get(edges): lines.append() for edge in data.get(edges, []): label edge.get(label, ) tail f : {label} if label else lines.append(f{edge[from]} - {edge[to]}{tail}) return \n.join(lines) _BOX {tl: ┌, tr: ┐, bl: └, br: ┘, h: ─, v: │} def _box_line(text: str, width: int) - str: return _BOX[v] text.ljust(width - 4) _BOX[v] def render_box(data: Dict) - str: Unicode 盒式框图适合架构图和分层图。 nodes data.get(nodes, []) if not nodes: return (empty) width max(24, max(len(n.get(label, )) for n in nodes) 10) width min(width, 60) lines [] for idx, node in enumerate(nodes): label node.get(label, node.get(id, )) lines.append(_BOX[tl] _BOX[h] * (width - 2) _BOX[tr]) lines.append(_box_line(label, width)) lines.append(_BOX[bl] _BOX[h] * (width - 2) _BOX[br]) if idx len(nodes) - 1: lines.append(_BOX[v].rjust(width // 2)) lines.append(▼.rjust(width // 2)) return \n.join(lines) def render_svg(data: Dict) - str: 极简 SVG 渲染适合嵌入 Markdown 文档。 nodes, edges data.get(nodes, []), data.get(edges, []) cols 3 box_w, box_h, gap_x, gap_y 160, 60, 40, 30 width cols * box_w (cols 1) * gap_x rows max((len(nodes) cols - 1) // cols, 1) height rows * box_h (rows 1) * gap_y pos {} parts [ fsvg xmlnshttp://www.w3.org/2000/svg width{width} height{height} fviewBox0 0 {width} {height} font-familysans-serif, defs marker idarrow markerWidth8 markerHeight8 refX6 refY3 orientauto path dM0,0 L6,3 L0,6 Z fill#9ca3af/ /marker /defs, ] for i, node in enumerate(nodes): x gap_x (i % cols) * (box_w gap_x) y gap_y (i // cols) * (box_h gap_y) pos[node[id]] (x box_w // 2, y box_h // 2) parts.append( frect x{x} y{y} width{box_w} height{box_h} rx8 ffill#f5f7ff stroke#4a6cf7 stroke-width1.5/ ) parts.append( ftext x{x box_w // 2} y{y box_h // 2 5} text-anchormiddle ffont-size13 fill#1f2937{node.get(label, node.get(id, ))}/text ) for edge in edges: frm, to pos.get(edge.get(from)), pos.get(edge.get(to)) if not frm or not to: continue parts.append( fline x1{frm[0]} y1{frm[1]} x2{to[0]} y2{to[1]} fstroke#9ca3af stroke-width1.5 marker-endurl(#arrow)/ ) parts.append(/svg) return \n.join(parts) # ---------- 3. 对外入口 ---------- RENDERERS {ascii: render_ascii, box: render_box, svg: render_svg} def render(data: Dict, format: str ascii) - str: format (format or ascii).lower() if format not in RENDERERS: raise ValueError(f不支持的格式: {format}可选值: {, .join(RENDERERS)}) return RENDERERS[format](data) def main(): parser argparse.ArgumentParser(descriptionshow_me 技能渲染器) parser.add_argument(--data, -d, helpJSON 数据或 A - B 文本) parser.add_argument(--format, -f, defaultascii, choiceslist(RENDERERS.keys())) args parser.parse_args() data parse_input(args.data or A - B; B - C) print(render(data, args.format)) if __name__ __main__: main()这段代码有几个设计点值得说明parse_input同时支持 JSON 和A - B文本降低了调用方的心智负担。三种渲染器都只接收统一的nodes edges结构方便后续扩展新格式。SVG 渲染没有使用任何第三方库纯字符串拼接体积小、可读。渲染器提供了命令行入口方便调试和集成。5.4 编写 Agent 接入端接下来写一个极简的 Agent 入口演示技能注册和路由。文件路径为agent.py# 文件路径agent.py 极简 Agent 演示把 /show-me 作为技能挂载到 Agent 上。 注意这是一个演示用的规则式 Agent真实项目中通常由 LLM 根据用户意图自动选择技能这里用关键词触发来降低理解成本。 from pathlib import Path from