1. 项目起源:先说我为什么要做图推提示图
先说结论:我最近做了个小项目,名字叫图推提示图,核心功能不是做一张静态提示卡,而是用图结构推理的方式,把提示词工程里最核心的“下一步该怎么走”自动画成一棵提示路径图,告诉用户从当前输入出发,经过哪些节点、哪些分支、哪几组指令,能落到一个更靠谱的提示词结果上。
这个标题乍一看有点绕,拆开解释就清楚了。图推代表的是图推理,也就是把提示词之间的前后依赖关系建造成一张有向图,然后根据用户输入去图上做检索和路径推荐;提示图则是一张可视化的图谱,用节点表示提示步骤,用边表示流转条件,最终输出给用户看的不是一段干巴巴的文字,而是“图”和“文字”混合的提示卡片。
我为什么想折腾这个?因为日常工作里接触很多提示词相关场景,团队里也一直有人在问:给一个主题,我到底先写什么后写什么?为什么别人写的提示词效果稳定,我写的就老是漏边界条件?这类问题靠背模板解决不了,因为一次对话的走向不是线性的,而是需要在多个节点里跳跃、回退、补充。而图结构恰恰适合描述这种多岔路的流程。
这个项目适合三类人参考:第一类是正在做提示词工程,但总觉得“模板不够用”的开发者;第二类是做大模型应用,想引入结构化知识管理和路径推荐的产品工程师;第三类纯粹是想用图数据库、图算法做点实际工具的学习者。看完之后,最少能收获一套可以落地的图推提示图搭建思路,以及我在实际调试阶段踩过的坑。
2. 为什么用“图”而不是用“表”去组装提示词
2.1 提示词的本质是一个多路径决策问题
提示词表面上是文本拼接,但真正复杂的提示词更像是一套决策流程。举个例子,我要生成一份招聘 JD 的提示词,背后至少要经过角色设定、岗位职责、任职要求、福利亮点、加分项约束这几个节点。不同节点之间还有条件分支:如果是技术岗,加分项节点需要细化到框架版本;如果是市场岗,则要把结果量化要求提前。
表格能存这些信息,但表格只适合表达扁平结构。放进 Excel 的提示词模板,列一多就乱,条件一多就重复,维护成本直线上升。图结构天然表达的是实体和关系,节点存“提示模块”,边存“流转条件”,正好把决策路径建模成图,比如从根节点“意图识别”出发,根据输入关键词分别流转到“场景提示”“角色提示”“输出约束提示”,这种关系用图来表达是一目了然的。
2.2 图推理带来的三个直观收益
用图推理做提示图,不只是换了个存储方式,它带来几个很实际的收益。
第一,路径可解释。提示词生成的结果为什么包含某个模块?因为图上有一条从“意图”到“模块”的路径。用户可以把鼠标悬停在路径上,看到流转条件是“关键词命中岗位类型=技术”,这样比黑盒生成更容易排查问题。
第二,提示图可复用。同一张图可以喂给不同输入。输入不同,图推理跑出来的路径也不同,但图本身不用重新画。这样团队里的提示资产就沉淀下来了,而不是散落在各种 Markdown 文档里。
第三,推理可审计。我之前用纯大模型拼接提示词,经常出现“昨天能用今天不能用”的情况。换成图推提示图之后,每次推理产生的节点顺序和触发条件都能记录下来,差在哪里一眼就能发现。对于要求稳定输出的生产场景,这个价值比节省几百 token 高得多。
3. 核心设计拆解:图推提示图的整体链路
3.1 三层结构:数据层、推理层、展示层
整个项目我没有搞复杂架构,就是一套清晰的三个模块:数据层负责把提示词内容节点化,推理层负责根据输入做路径检索,展示层负责把推理结果渲染成提示图。每层干的事情越单一,后续维护就越舒服。
数据层我选择了纯 JSON 加 NetworkX 的方式,原因很简单:项目初期数据量不大,没必要引图数据库。每条提示节点包含节点 ID、节点名称、触发关键词、通用指令内容、示例内容等字段。节点之间的关系也存为数组,每条关系声明 source、target、trigger 三件事。
推理层是核心。我先做意图拆解,把用户输入切分成几个属性字段,然后在图上做带条件的广度优先搜索。每一步只保留触发条件命中的邻接节点,如果一个节点有多个候选子节点,则按照预设的置信度排序,不满足条件的分支直接剪掉。
展示层我一开始用 Graphviz,后来换成 pyvis,因为 pyvis 导出的 HTML 支持交互,鼠标悬停能看到节点详情,也可以用下拉框切换不同路径。现场演示的时候效果好得多。
3.2 节点类型怎么设计才不臃肿
节点类型一开始设计得越细越好是错觉,实际做下来我建议尽量收敛。我把全部节点划成四类:入口节点、处理节点、约束节点、输出节点。
- 入口节点:对应意图识别,根节点只有一个,用来承接用户原始输入。
- 处理节点:表示对内容做某种转换,比如“拆分文章结构”“抽取关键实体”“生成示例”。
- 约束节点:负责给提示词加上边界限制,比如“禁止编造数据”“输出格式必须为 JSON”。
- 输出节点:表示最终提示词结果的组装位置,每个输出节点挂一段模板字符串。
用四类节点覆盖所有场景有一个好处:后续做图推理时,约束节点会被自动附加到所有路径末尾,不需要为每个分支单独写约束。项目里我实际建了 30 多个节点,即便再翻一倍,维护成本也还可控。
4. 实操过程:从空目录到能跑的图推提示图
4.1 技术选型与依赖安装
我是在 Python 3.10 环境下实现的。依赖只用了五个,全部通过 pip 安装:
pip install networkx pyvis jieba rapidfuzz pandas这里说明一下为什么选这些工具。NetworkX 用来存储和遍历图,自带多种图算法,不需要自己造轮子;PyVis 用来生成可视化 HTML;jieba 做中文分词,方便从用户输入里抽取关键实体;rapidfuzz 用于模糊匹配,当用户输入的关键词和节点触发词不完全一致时也能找到最接近的节点。
图数据库我暂时没引入。项目当前节点数不超过一百个,内存图完全够用。如果后续要支持上万的图谱规模,我可以把 NetworkX 替换成 Neo4j,但接口层我已经做好了隔离,换起来不会伤筋动骨。
4.2 数据建模:拿着这份 JSON 就能开始建图
下面这张图的数据模型是项目里实际用到的简化版,节点和边的写法都有清单,直接复制就能跑。
# prompt_nodes.json { "nodes": [ {"id": "INTENT", "type": "entry", "name": "用户意图"}, {"id": "TOPIC_PARSE", "type": "process", "name": "主题解析", "keywords": ["主题", "题目", "文章"]}, {"id": "ROLE_SET", "type": "process", "name": "角色设定", "keywords": ["角色", "身份", "扮演"]}, {"id": "FORMAT_JSON", "type": "constraint", "name": "JSON格式约束", "content": "只输出JSON对象,不要包含解释性的文字。"}, {"id": "OUTPUT_TEMPLATE", "type": "output", "name": "最终提示词模板", "template": "请以{role}的身份,围绕{theme}完成以下任务,并严格遵循{format_rule}"} ], "edges": [ {"source": "INTENT", "target": "TOPIC_PARSE", "trigger": "has_topic"}, {"source": "TOPIC_PARSE", "target": "ROLE_SET", "trigger": "has_role"}, {"source": "ROLE_SET", "target": "FORMAT_JSON", "trigger": "need_json"}, {"source": "FORMAT_JSON", "target": "OUTPUT_TEMPLATE", "trigger": "always"} ] }节点字段里,keywords是触发词列表,template是输出组装模板,content是约束内容。边的trigger字段是流转条件,取值为布尔表达式的字符串。设计时的原则是一条边只表达一个核心关系,宁可使用两个节点加两条边,也不用复杂逻辑混在一条边里,这样图推理的时候更省心。
4.3 核心推理代码实现
图推理我封装成了一个只有两个核心方法的类。第一个方法负责把用户输入映射到初始节点,第二个方法负责沿着图做路径搜索,收集所有可达输出节点。完整代码如下:
import json import networkx as nx from rapidfuzz import fuzz from typing import Dict, List class PromptGraph(NxProvider): def __init__(self, graph: nx.DiGraph, node_data: Dict): self.graph = graph self.node_data = node_data @classmethod def load(cls, graph_json: Dict) -> 'PromptGraph': graph = nx.DiGraph() for node in graph_json["nodes"]: graph.add_node(node["id"], **node) for edge in graph_json["edges"]: graph.add_edge(edge["source"], edge["target"], trigger=edge["trigger"]) return cls(graph, {node["id"]: node for node in graph_json["nodes"]}) def parse_intent(self, user_input: str) -> Dict[str, bool]: intent = {"has_topic": False, "has_role": False, "need_json": True} if any(kw in user_input for kw in ["主题", "题目", "文章"]): intent["has_topic"] = True if any(kw in user_input for kw in ["角色", "扮演", "身份"]): intent["has_role"] = True return intent def recommend(self, user_input: str) -> List[str]: intent = self.parse_intent(user_input) start_node = "INTENT" path = [] queue = [start_node] visited = set() while queue: current = queue.pop(0) if current in visited: continue visited.add(current) path.append(current) for nxt in self.graph.successors(current): edge = self.graph.edges[current, nxt] trigger = edge.get("trigger", "") if trigger == "always" or intent.get(trigger, False): if nxt not in visited: queue.append(nxt) return path代码里的parse_intent是一个粗糙的意图提取实现,实际项目中我建议替换成小型分类模型或者大模型结构化输出,但思路是一样的:把输入变成一组布尔属性,再让图的边去匹配属性。
recommend方法采用广度优先遍历,边触发条件不满足的邻接节点会被直接跳过。因为图本身是有向无环的,这个写法不用担心死循环。输出结果是路径节点列表,顺着列表取name和template字段,就能直接组装成最终的提示词。
4.4 提示图可视化与提示卡输出
拿到路径列表之后,我做了两件事。第一件事是渲染提示图,第二件事是拼接提示卡。渲染提示图用 PyVis:
from pyvis.network import Network def render_prompt_graph(graph: PromptGraph, highlight_path: List[str] = None): net = Network(directed=True, height="600px", width="100%") for node_id, data in graph.node_data.items(): color = "#ffd666" if node_id in highlight_path else "#d9d9d9" net.add_node(node_id, label=data.get("name", node_id), color=color) for edge in graph.graph.edges(data=True): net.add_edge(edge[0], edge[1], label=edge[2].get("trigger", "")) net.show("prompt_graph.html") return "prompt_graph.html"这样生成的 HTML 可以直接用浏览器打开,高亮路径就是图推理选中的路线。鼠标悬停在节点上能看到type和keywords这些详情字段。
提示卡拼接则简单很多,遍历路径节点里的 output 节点,把模板里的占位符替换成用户输入,再拼上所有约束节点的 content,形成最终文本。我会把提示图放在卡片上方,文本放在下方,用户第一眼看到路径,第二眼看到可复制的提示词,这个体验比单独丢一段文字好很多。
5. 实测效果:三种输入跑出来的不同路径
5.1 完整的主题创作场景
输入是“帮我准备一篇关于人工智能发展趋势的主题文章,请以科技博主身份来写”。
意图解析结果为has_topic=True,has_role=True,need_json=True。图推理跑出来的路径是:
INTENT → TOPIC_PARSE → ROLE_SET → FORMAT_JSON → OUTPUT_TEMPLATE
这个结果里,“科技博主身份”被正确匹配到 ROLE_SET,“主题文章”匹配到 TOPIC_PARSE,JSON 格式约束被自动追加。因为路径里缺少专门的示例节点,所以没有触发示例注入。后续我发现专业性不足时,就在 TOPIC_PARSE 后面又加了一个“示例注入”分支节点,精准补齐。
5.2 缺少关键信息的场景
输入只有“帮我写个文案”的时候,没有命中任何主题关键词。图推理只走到 TOPIC_PARSE 就停住了,因为 TOPIC_PARSE 之后的边触发条件has_topic为 false。
这时候系统不是硬着头皮往下组装,而是返回一个“提示图缺口”:用户被明确告知缺少主题信息,提示图上 TOPIC_PARSE 这个节点会被标成红色。这个功能特别适合作为交互式问卷的前置模块,用户照着缺口逐个补信息,比一次性问五个问题友好得多。
5.3 多分支并行场景
当输入里同时包含“角色”“示例”“字数”三个属性的时候,TOPIC_PARSE 的多个子节点全部被激活,路径会自动形成并行分支。我在展示层做了分组渲染,把并行分支放在同一层,用箭头并列指向后续节点。
实测下来的效果是:提示图变成了真正的图,而不是线性步骤条。用户能看到哪些模块可以并行补充,哪些模块必须按顺序执行。这个信息在纯文本提示词里是拿不到的。
6. 避开过的坑:图推提示图实战踩坑记录
6.1 触发条件太弱导致路径爆炸
第一次跑测试时,我给的触发条件全是always,结果每个节点都跑到,路径变成一棵扇形树,提示图几乎没有信息量。后来我把边的触发条件收敛成has_topic、has_role、need_json这类布尔变量,路径才变得有意义。
排查这个问题的办法其实很朴素:把每条边的 trigger 列出来,如果一个节点出度超过 2,且出边 trigger 高度相似,基本可以判断条件设计有问题。
6.2 中文分词把专业术语拆碎
我一开始用的 jieba 默认词典,结果“大语言模型”被拆成“大语言”和“模型”,节点匹配不稳定。后来我做了两步修复:第一步把专业术语加进用户词典,避免误切;第二步用 rapidfuzz 做局部匹配,即使切词不完美,只要子串能在关键词里找到,也能命中节点。
6.3 可视化图的节点位置混乱
NetworkX 的 spring_layout 布局对小图很友好,但节点一多,重叠严重。PyVis 默认给的层次化布局有时候也会把根节点放错位置。我的解决方式是自定义了分层布局:入口节点在第 0 层,处理节点在第 1 层,约束节点在第 2 层,输出节点在第 3 层,层内做横向均匀排布。这样视觉上稳定得多,截图也更专业。
6.4 提示图数据更新时的死链
有一次我删除了一个中间节点,忘了删对应的边,导致 NetworkX 在你访问边时直接抛异常。后来我在加载数据时加了一个校验函数,自动检测所有边引用的节点是否存在,缺失时打印清晰错误信息并把这条边记入日志,不轻易中断进程。
6.5 不要忽略路径记忆功能
单纯做路径推荐还不够,实际使用中我发现用户经常想知道“为什么这次没有走到某个节点”。后来我加了路径日志,每次推理都存下输入、意图中间结果、推荐路径、触发条件四个字段。排查问题时,回看日志会发现很多模糊匹配失误是由意图解析误判导致的,这比直接改图结构高效得多。
7. 工具选型和后续扩展的思路
图推提示图的技术栈不复杂,但选型时务必考虑一个问题:你未来要处理的图规模有多大。如果只做提示词管理,NetworkX 完全够;如果将来要引入上万节点、实时协同更新、复杂图算法,建议换成 Neo4j 或者 NebulaGraph,并把节点数据从 JSON 文件迁移到数据库。
我做接口隔离时,把图操作都封装在PromptGraph类里,内部用的是 NetworkX,但外层用方不需要关心。后续要么把 NetworkX 的操作适配成图数据库查询,要么用 Service 层拦截,替换成本都相对低。
再有,顺带聊聊大模型在这里的角色。我当时没有把大模型引入核心推理链路,而是让它只负责意图解析和模板润色,原因是大模型的不确定性和图推理的确定性能互补。图推理保证路径结构稳定,大模型负责让路径上的文本更自然。两者结合的效果,比单纯让大模型输出全部提示词要稳定得多。
8. 最后分享几个我反复用到的小经验
项目写到这里,核心内容已经收尾了,我再补充几个实践中反复用到、浓到没必要单独开章节的点。
调用图推提示图接口时,记得把输出路径和提示词文本分成两个字段返回。前端使用者通常需要两种模式:只看图,或者只复制文本。混在一起会让交互做得很别扭。
提示图的节点命名不要用英文缩写,我一开始用了很多类似TRF、OUTX的缩写,结果团队成员根本不记得代表什么。后来统一改成主题解析、输出模板这类中文可读名称,沟通顺畅了很多。
建图的时候每次新增节点,问自己一个问题:节点之间的边触发条件放到自然语言里到底能不能解释清楚?解释不清楚就说明设计太复杂了,建议拆节点。
事实上我在做这个图推提示图之前,也用过纯文本模板和规则拼接,它们的问题不是“做不出来”,而是“维护不上”。提示词从五个模块涨到十五个模块时,规则拼接的代码就开始堆 if-else,加一个条件可能要改四个文件。图推提示图把这种复杂度转移到了数据上,改图结构不需要动逻辑代码,这是它最打动我的地方。
如果你也在做类似的提示词管理或者工具推荐,我建议直接参照这套图推和可视化方案试一个最小版本,数据量控制在二十个节点以内,跑通之后你会直观地感受到“提示词从脚本变成了资产”这件事。