这两年做知识问答类应用,几乎所有人都会撞上同一堵墙:业务知识是一张网,而不是一堆文本。实体之间的多跳关系、带条件的统计口径、路径型问题,靠向量召回根本说不清;而手工维护上千条查询模板又不现实。于是就有了标题里这个组合——GraphRAG + Text2Cypher。GraphRAG 负责把知识组织成图谱,Text2Cypher 负责把自然语言问题翻译成图查询语言,让大模型自己去图里“查表”。这篇文章想把整条链路从背景到落地彻底拆开:为什么偏偏需要它、大模型生成 Cypher 的底层逻辑是什么、工程上哪些细节最容易翻车、以及我实测几轮之后沉淀下来的优化打法。
1. GraphRAG 为什么偏偏需要 Text2Cypher —— 一张网逼出来的真问题
1.1 向量RAG 在图谱场景下的颗粒度矛盾
传统 RAG 的思路很简洁:把知识库切成几百字的片段,向量化存起来,用户提问时做 top-k 召回,再把片段丢给大模型总结。这套思路在“找一段相似文字”的场景下非常顺,但你只要往深度推理走半步就会露馅。
拿一个典型问题举例:“帮我找出所有跟张三合作过、而且目前在做软件行业的人。”这个问题本质是个图模式查询——张三是一个节点,合作是一条关系,软件行业是另一个节点的属性约束。向量召回能给你什么?它可能给你一篇关于张三的介绍、一篇关于某合作项目的文档,甚至一篇行业分析报告。它能拼出“张三 -> 合作 -> 人 -> 软件行业”这条路径吗?拼不出来。因为你查询的目标不是一个语义块,而是一段关系结构。
我在给某数据分析平台做知识检索升级时就卡在这里。当时团队先用常规向量方案做了一版,评测结果很惨:单跳问题还能蒙对,两跳以上正确率直接掉到三成。后来我意识到,问题的本质是颗粒度不匹配——切块后的文本保留了“段落语义”,却丢掉了“实体之间的关系”。知识如果是一张网,切块就相当于把网剪成了线头,再怎么检索也是断的。
1.2 Text2Cypher 在 GraphRAG 链路中的位置
GraphRAG 做的事,是先把零散文档或者结构化数据整理成一张实体关系图:实体是节点,实体之间的联系是带类型的边。知识已经有了图结构,剩下的事情就是“如何问图”。
Text2Cypher 就是这个“问图”的转换器。完整链路大致是这样的:
用户问题 -> 程序做必要预处理 -> 大模型生成 Cypher 查询 -> 图数据库执行 -> 拿到结构化的结果 -> 再交给大模型整理成自然语言答案。
这里的核心变化是:大模型不再直接从文本片段里“猜”答案,而是负责把问题翻译成精确的查询语句,真正的取值和计算交给图数据库。翻译正确,答案就有据可查;翻译错误,至少我们能通过校验及时发现,而不是看着模型一本正经地编造。
你可以把 Text2Cypher 理解成“语义到结构化查询”的翻译器。它的难点不在于语法,而在于让模型始终在你的图模式、你的属性命名、你的业务规则里做选择题,而不是自由发挥。
1.3 什么时候才值得上这套方案
不是所有项目都需要 GraphRAG + Text2Cypher。我判断一个场景适不适合,一般看三个条件:
第一,数据本身适合建模成图。实体明确、关系丰富、经常要做多跳查询,图是天然的载体。如果数据只是一堆无结构的说明文档,硬建模会很痛苦。
第二,查询模式足够多变。用户今天问“张三名下所有项目”,明天问“哪些公司和张三有过合作且不在北京”,后天问“近三个月合作次数最多的前五个人”。这种组合式问题,模板根本写不过来,但 Text2Cypher 可以灵活组合。
第三,团队能接受一定的不确定性。大模型生成的 Cypher 不可能 100% 正确,必须有校验和纠错环节兜底。如果业务要求每条查询都必须精确无误、不允许任何波动,那老老实实用规则模板,别折腾。
我用一张表总结一下适用边界:
| 场景特征 | 适合 Text2Cypher | 更适合模板/规则 |
|---|---|---|
| 查询变化频率 | 高,组合型问题多 | 低,几十条固定查询 |
| 数据关系复杂度 | 多跳、多类型关系 | 简单一对一关系 |
| 数据量 | 中等以上,人工难以枚举 | 数据量小,人工可管理 |
| 允许试错 | 可以接受少量错误并迭代 | 不允许任何错误 |
| 人力成本考量 | 模板维护成本高于模型调优成本 | 模板维护成本很低 |
这套方案适合的是“查询风暴式”的知识图谱场景,人写模板写不过来,机器翻译又有校验可兜底。方向选对了,后面每一步才有意义。
2. 大模型写 Cypher 的底层逻辑:不是在背语法,而是在做被约束的翻译
2.1 Cypher 的语法骨架:节点、关系、模式
很多第一次接触 Cypher 的同学会把它当成 SQL 的变体,这是个误区。Cypher 的核心概念是“模式匹配”——你描述一段图上要匹配的形状,数据库沿着形状找数据。最基础的形状是节点,其次是节点加关系,再组合成路径。
MATCH (p:Person {name: "张三"})-[:KNOWS]->(friend:Person) RETURN friend.name这行查询的意思很直白:在图上找所有和“张三”之间存在 KNOWS 关系的 Person 节点,返回他们的 name 属性。圆括号表示节点,方括号表示关系,箭头表示方向,花括号可以放属性过滤条件。
再复杂一点,聚合和排序也长得和 SQL 不太一样:
MATCH (p:Person)-[:WORKS_AT]->(c:Company) WHERE c.industry = "软件" RETURN c.name, count(p) AS person_count ORDER BY person_count DESC LIMIT 10对于生成侧来说,我们真正要让模型掌握的只有几个原子能力:节点属性过滤、关系路径匹配、条件过滤(WHERE)、聚合(count/collect)、排序分页(ORDER BY / LIMIT)、以及可选的排除逻辑(NOT EXISTS / NOT IN)。这几种能力组合起来,已经能覆盖绝大多数业务问题。不需要模型去写复杂的图算法语句,那超出 Text2Cypher 的职责范围了。
2.2 Schema 是生成的命门
这是整篇文章我最想强调的一点:大模型根本不知道你的图里有什么。它没见过你的节点标签,不知道你的属性叫birth_date还是birthday,更不知道你的关系类型是KNOWS还是HAS_RELATION_WITH。你如果不把图模式喂给它,它就只能凭训练时见过的通用常识瞎猜——而猜出来的实际执行结果,往往是一条报错。
所以每次调用模型生成 Cypher 之前,必须把当前图谱的 Schema 信息注入到 Prompt 里。所谓 Schema,就是图的一张“体检表”:
节点: - Person: name, age, job_title, company_id - Company: name, industry, founded_year - Project: name, status, start_date, end_date 关系: - Person[:WORKS_AT]->Company - Person[:KNOWS]->Person - Person[:PARTICIPATES_IN]->Project有了这张表,模型才能做出正确的选择:用户问“张三年初开始参与的项目”,模型会知道应该去PARTICIPATES_IN关系里找,而且可以用Project.start_date做时间过滤,而不是自己发明一个join_time属性。
Schema 信息的完整性和准确性,直接决定了生成质量的底线。我后来做了一个脚本,直接从图数据库里把节点标签、属性名、关系类型和方向拉出来,自动拼进 Prompt,避免人工维护 Schema 文本导致过期。这一步省心效果极为明显。
2.3 Few-shot 示例怎么设计才有效
光有 Schema 还不够,模型还需要看几个“问题到 Cypher”的映射样例,才能理解你的表达风格和业务口径。Few-shot 示例的设计有几个原则。
第一,覆盖查询类型,而不是覆盖业务。你可以准备六到八个样例,分别代表属性过滤、单跳关系、多跳路径、聚合统计、排序分页、排除查询。业务问题千变万化,拓扑结构就那么几类。
第二,每个示例都要写得“像真的一样”。示例里的节点名、属性名必须和 Schema 严格一致,不能随手编一个 Schema 里不存在的字段。否则模型会学坏。
第三,示例数量控制在 8 个以内。我试过塞 20 个示例,效果反而下降——模型会过度拟合示例的句式,把用户问题硬套进某个模板里。
下面是我常用的一组示例结构:
| 示例问题 | 示例 Cypher | 考察点 |
|---|---|---|
| 张三在哪个公司工作 | MATCH (p:Person {name:$name})-[:WORKS_AT]->(c:Company) RETURN c.name | 属性过滤 + 单跳关系 |
| 张三合作过的人都参与了哪些项目 | MATCH (p:Person {name:$name})-[:KNOWS]->(friend)-[:PARTICIPATES_IN]->(proj:Project) RETURN DISTINCT proj.name | 多跳路径 + 去重 |
| 各公司参与项目的人数 | MATCH (c:Company)<-[:WORKS_AT]-(p:Person)-[:PARTICIPATES_IN]->(proj:Project) RETURN c.name, count(DISTINCT p) AS cnt | 聚合统计 |
| 参与项目数最多的前三个人 | MATCH (p:Person)-[:PARTICIPATES_IN]->(proj:Project) RETURN p.name, count(proj) AS cnt ORDER BY cnt DESC LIMIT 3 | 排序分页 |
| 没有参与任何项目的人 | MATCH (p:Person) WHERE NOT EXISTS { (p)-[:PARTICIPATES_IN]->(:Project) } RETURN p.name | 排除查询 |
注意一个细节:示例里的用户问题最好用中文,查询里的返回字段名用英文。模型在这里学到的其实是“中文问题 -> 图结构映射”的能力,而不是翻译能力。
2.4 让模型只输出一行可执行的 Cypher
很多初版实现会把 Prompt 写成“请生成一条 Cypher 查询”,然后模型在回答里来一段解释、加一个 Markdown 代码块、甚至最后补一句“希望这个答案对你有帮助”。跑去解析这种输出痛苦不堪。
我现在的做法是强制模型输出 JSON 结构:
{ "query": "MATCH (p:Person {name: $name})-[:WORKS_AT]->(c:Company) RETURN c.name", "params": {"name": "张三"} }Prompt 里直接写明:“只输出 JSON,不要输出任何其他内容,不要 Markdown 代码块,query 值必须是可执行的 Cypher 字符串,params 是参数字典。”这样程序拿到结果后一次json.loads()就完事,干净利落。
另外,强烈建议让模型生成参数化查询,而不是把实体值直接拼进字符串。比如用户问“张三在哪个公司”,模型应该输出{name: $name}配合params: {"name": "张三"},而不是输出{name: "张三"}。
这样做有两个好处:一是避免用户输入里的特殊字符破坏查询结构,二是为后续做查询缓存和权限控制留出空间。把生成和取值分离,是这套方案走向工程化的第一步。
3. 从零搭起一套可交付的 Text2Cypher 链路
3.1 技术选型与运行环境
先说明一下我常用的技术组合:图数据库选择支持 Cypher 方言的即可,开源部署或云托管都行,关注点是能否提供只读账号和EXPLAIN语法校验接口;大模型选通用推理 API 或者本地部署的模型,只要上下文窗口能容纳 Schema 和示例就行,不一定需要顶配,因为生成查询属于结构化任务,大部分场景下小一号的模型加好的 Prompt 反而更稳。
运行环境上,我用 Python 3.10+,配合图数据库官方 Driver。发请求的部分可以自己写,也可以用一个编排框架,区别不大,但建议不要把逻辑绕进太重的框架里——Text2Cypher 的核心路径很短,自己掌控每一步更踏实。
另外强烈建议准备两个独立的 Python 脚本:一个是schema_export.py,负责把图数据库的 Schema 拉出来生成文本;一个是text2cypher.py,负责调用模型、校验、执行。两个脚本职责分离,后续排查问题会快很多。
3.2 建模与建图:好图谱才有好查询
Text2Cypher 的上限其实在建图阶段就定死了。如果图谱本身建模混乱,模型再聪明也白搭。我总结了三条建模约定,团队内部最好当规范执行。
第一,节点标签用名词单数大驼峰。标签是模型需要直接引用的名字,Person、Company、Project这种一眼能看懂的最好,千万别用NODE_TYPE_A这种没有语义的代号。
第二,属性名统一小写下划线命名。birth_date、job_title、start_date是好的;birthDate、BirthDate这些写法会让模型非常容易记错。尽量让属性名和常见英语词汇对齐,因为模型的记忆里天然偏向某个词的常见形态。
第三,关系必须有方向,且类型名用动词过去式。模型在翻译“张三在哪个公司工作”时,如果关系类型是works_at,它几乎不会判断错方向。如果用RELATED这种无含义的类型,模型每次都在猜。
建图时还要把唯一约束建好。例如 Person 节点的 name 属性加唯一约束,否则重复节点会让查询结果翻倍,模型看到返回的数据也会困惑。约束语法大致长这样:
CREATE CONSTRAINT person_name_unique IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE之后导入数据,我习惯从结构化表格或者文档抽取的实体关系三元组开始,先在测试库上跑通几个典型查询,再切生产。千万不要跳过这一步直接上模型——模型生成的错误你可能要花几小时才能排查出来,而图谱本身的建模问题会放大一切错误。
3.3 Prompt 模板的完整示例
下面是一份可以直接复制改用的 Prompt 模板:
## 角色 你是一个将自然语言问题转换为 Cypher 查询的专业助手。 ## 图数据库 Schema 节点: - Person: name, age, job_title, company - Company: name, industry, founded_year - Project: name, status, start_date, end_date 关系(箭头表示方向): - Person[:WORKS_AT]->Company - Person[:KNOWS]->Person - Person[:PARTICIPATES_IN]->Project ## 示例 问题:张三在哪个公司工作? Cypher: {"query": "MATCH (p:Person {name: $name})-[:WORKS_AT]->(c:Company) RETURN c.name", "params": {"name": "张三"}} 问题:各公司参与项目的人数是多少? Cypher: {"query": "MATCH (c:Company)<-[:WORKS_AT]-(p:Person)-[:PARTICIPATES_IN]->(proj:Project) RETURN c.name, count(DISTINCT p) AS cnt", "params": {}} ## 规则 1. 只输出 JSON,格式为 {"query": "...", "params": {...}} 2. 必须使用 Schema 中存在的节点标签、属性名和关系类型,禁止自行发明 3. 实体值一律放入 params,不要直接拼接进 query 字符串 4. 不要返回任何解释或多余文字 ## 用户问题 {question}这个模板能稳定工作的原因,在于它的顺序有讲究:角色声明让模型进入“只写查询”的模式;Schema 紧跟其后,让模型在动笔之前先“看到地图”;示例排在 Schema 后面,模型会先内化结构再学映射;最后的规则是硬性约束,把输出格式和禁止事项钉死。
我踩过的一个坑是:把示例放在角色声明前面,模型偶尔会先模仿示例的语气说话,最后生成的内容里混进去一段“好的,我来帮你写查询”。顺序调整之后这个问题基本消失。
3.4 生成 → 校验 → 执行 → 修正的闭环
模型输出的东西不能直接执行。哪怕是 GPT 级别的模型,也可能生成一个属性名拼写错误或者方向相反的查询。所以核心流程必须是一个闭环:先生成,再校验,执行失败就带错误信息回去重试。
校验分两层。第一层是静态检查,在程序里过滤危险子句。第二层是语法校验,用图数据库的EXPLAIN机制跑一遍不实际执行,确认语句能解析通过。
伪代码大概是这样:
SAFE_PATTERNS = ["MATCH", "RETURN", "WHERE", "ORDER BY", "LIMIT", "COUNT", "DISTINCT"] BLOCKED_KEYWORDS = ["CREATE", "MERGE", "SET", "REMOVE", "DELETE", "DROP", "ALTER"] def generate_and_execute(question, schema, examples, max_retry=2): error_info = "" for attempt in range(max_retry): cypher_json = call_llm(build_prompt(question, schema, examples, error_info)) query = cypher_json["query"] params = cypher_json.get("params", {}) if not static_check(query): error_info = "查询包含被禁止的子句" continue try: with driver.session() as session: session.run("EXPLAIN " + query) # 语法校验,不实际执行 result = session.run(query, **params) return preprocess_result(result) except Exception as e: error_info = str(e) # 把错误信息回传给模型 continue raise RuntimeError("多次重试后仍然失败")关于空结果的处理,单独说一句:返回空不一定是生成错误,也可能是问题本身没有答案。我的策略是,只有“执行报错”才带错误信息重试;空结果直接进入宽松化逻辑——去掉 LIMIT、去掉排序、只保留核心过滤条件,再跑一次。这样做能在“模型写错”和“数据不存在”之间做一个很好的平衡。
3.5 结果后处理:把查询记录交给下游
图数据库返回的原始结果往往是节点对象、关系对象或者纪录行,不能直接塞给大模型做总结。后处理这一步主要做三件事:转成纯字典结构、删掉内部元数据、保留业务字段。
比如执行上述查询后,返回的record.data()可能是:
[ {"c.name": "星辰科技", "person_count": 12}, {"c.name": "远航软件", "person_count": 8} ]节点对象会被自动展开成属性字典,person_count这种聚合值则直接保留。如果查询返回的是实体节点而不是标量,我会把它转成{"label": "Person", "properties": {...}}这种形式,既给后续总结模型提供了清晰的结构,也便于前端渲染。
后处理完成后,再带着“用户问题 + 查询结果”走一次通用大模型总结调用,它就能给出类似“参与项目人数最多的是星辰科技,有 12 人”这样的自然语言答案。到这一步,一条完整的 GraphRAG 问答链路才算闭环。
4. 实测中的五大坑,以及对应的优化方案
4.1 实体歧义:一个“苹果”问出两种结果
上线第一周我们团队就碰到了一个现象级的翻车:用户问“苹果的创始人是谁”,生成的查询五花八门——有人生成MATCH (p:Person {name:"苹果"}),有人生成MATCH (c:Company {name:"苹果"})。查询本身不报错,但结果要么为空,要么答非所问。
问题出在实体链接环节。模型光看“苹果”两个字,不知道它该匹配水果节点还是公司节点。解决思路是不要把“消歧”全部推给生成模型,而是把实体识别和链接放在前面:
先做 NER,识别出问题里的实体词;然后做一个实体链接的候选集操作,根据上下文把“苹果”映射到Company {name:"苹果公司"},或者映射到Fruit {name:"苹果"};最后把消歧后的实体作为参数传给生成模型。
如果你不想额外接入实体链接工具,至少要在 Prompt 的示例里增加一个负例,明确告诉模型:“当实体可能对应多个标签时,优先匹配用户问题中出现的领域词所属类别”。这能显著降低歧义率,但没法根治。条件允许的话,还是建议在库表里维护一张实体别名表。
4.2 属性名不存在,是错误率的最大来源
我跑过一组评测,模型生成的查询里属性名错误占比接近三分之一,高于语法错误和逻辑错误。原因很好理解:模型记忆里的常见字段名和图里实际的名字往往不一样。比如你的图里叫join_date,模型大概率会写start_date;你的图里叫mobile,它可能会写phone。
这个坑靠提示词能缓解,但没法根除。我的标准做法是加一个“属性名校验器”:解析生成的 Cypher,把里面出现的所有属性 token 提取出来,和 Schema 清单比对,一旦发现 Schema 里不存在的属性,直接拒绝执行并让模型重写,同时把错误信息“属性 xxx 不存在,可用的属性有 yyy”喂回去。
这个校验器实现起来很简单,用正则提取n.property形态的 token 就行,但收益极大。它把“模型能不能写对查询”从碰运气变成了一个可重复的工程流程。
4.3 深链路查询引发的性能事故
用户问“张三认识的人里,哪些人参与了某项目”,模型可能生成一条三跳查询,比如:
MATCH (p:Person {name:$name})-[:KNOWS*1..3]->(friend:Person)-[:PARTICIPATES_IN]->(proj:Project) RETURN DISTINCT friend.name这条查询在关系密集的图里会展开成一个爆炸性的结果集。我见过一次生产事故:一个 10 万节点的图,跑了这种三跳查询,直接把数据库连接池拖满。
优化思路分三层。首先是图数据模型层面,给节点和属性建索引,尤其是参与过滤的属性;其次是查询层面,在 Prompt 规则里限制默认最大深度为 1 到 2,更深的路径必须让用户明确说明;最后是执行层面,所有查询统一加LIMIT 50上限,并设置数据库查询超时时间。
一个经验法则:图谱不是越大越好,而是越“可导航”越好。如果大部分查询都用不到三跳以上,再多的深度只是给模型挖坑。
4.4 重复查询的成本黑洞
Text2Cypher 每次都要调大模型,成本比传统模板查询高一个数量级。上线后我发现很多用户会反复问类似的问题:“张三的公司”“张三在哪里工作”“张三的任职单位是什么”,本质上都是同一个查询模式。每次都让模型从头生成,纯粹浪费预算。
我加的方案是两层缓存。第一层是语义缓存:把用户问题做一次轻量标准化,去掉停用词、统一同义词、把识别出的实体归一化,然后作为 key 直接缓存最终的自然语言答案。第二层是查询模板缓存:如果问题命中了之前生成过的查询模式,就复用模板,只替换 params 里的实体值。
模板复用这层的收益很可观。比如“张三在哪个公司”和“李四在哪个公司”,虽然问题不同,但生成的 Cypher 结构完全一样,只是参数不同。缓存命中后,一次 LLM 调用就省下来了。
4.5 权限与安全:只读账号是底线
让大模型生成查询并直接执行,稍微想想就知道危险在哪:模型可能生成CREATE、MERGE、DELETE、DETACH DELETE等写操作子句;用户输入里的恶意文本可能被拼进查询字符串。这两类风险都是安全底线问题。
务必要做三件事。
第一,连接图数据库一律使用独立账号,只授予查询权限,禁止写入权限。图数据库的账号权限配置完成后,任何写操作即使模型生成出来,数据库也会直接拒绝。
第二,应用层静态检查所有生成的 Cypher,一旦发现CREATE、MERGE、SET、REMOVE、DELETE、DROP、ALTER这些子句,直接拦截重试,不让它进入数据库。
第三,坚持参数化查询,不允许把用户原文拼进查询字符串。这两件事配合起来,能把最危险的几类攻击路径全部堵住。
5. 持续迭代:从“能跑”到“好用”
5.1 收集错误样本,反哺 Prompt
Text2Cypher 不是一个写完就能放着不管的模块,它需要持续迭代。我建议把每一次执行失败或者答案不准的记录都留档,至少包含四个字段:用户原始问题、模型生成的查询、执行结果或报错信息、人工修正后的正确查询。
积累了两三百条样本之后,按错误类型做个聚类。你会发现高频问题就那么几类:实体歧义、属性名错误、关系方向错误、多跳路径过深。针对每一类,就往 Prompt 里补充一个成熟的 Correct 示例或者错误反例。每次改完 Prompt,回归跑一遍固定问题集,看错误率变化。
这一步本质上是把模型从“通用问答模型”训练成“懂你这套图的口译员”。坚持两周,查询成功率能明显上一个台阶。
5.2 用一批固定问题做回归测试
改 Prompt 最怕的是“修好了一个问题,带崩了三个老问题”。所以固定问题集非常重要。我维护了一份 40 条左右的评测集合,覆盖属性过滤、单跳、多跳、聚合、排序、排除、空结果等所有查询类型,每条都人工标注了标准 Cypher 和期望结果。
每次改动 Prompt 模板、Schema 描述或者示例后,跑一遍全集,统计三个指标:语法执行成功率、查询结果与标准结果的重合度、以及空结果率。其中“语义准确率”是最难量化但最值得看的——两条查询执行结果可能一样,但生成逻辑完全不同,长期来看只有语义准确的查询才稳定。
这个回归集不用太大,但必须稳定。它让你的优化有据可依,而不是凭感觉调 Prompt。
5.3 多轮对话里复用上下文
单轮问答跑通之后,下一步就是多轮。多轮场景如果不做处理,用户说“那和他合作过的人呢”,模型完全不知道“他”指谁,生成的查询里实体就空了。
最简单的处理方式是在对话轮次之间维护一个“已知实体表”。上一轮已经识别出的实体和标签信息,在下一轮 Prompt 里直接带上:
当前对话已知实体: - 张三 (Person标签) - 星辰科技 (Company标签) 用户问题:和他合作过的人都有谁?这样模型在生成查询时,就知道把“他”替换成Person {name:"张三"},而不是从零开始推断。这个上下文记忆方案虽然朴素,但非常有效,不用引入复杂的记忆框架,只要在会话对象里维护一个列表而已。
我自己的感受是,Text2Cypher 最难的其实不是让模型“写对语法”,而是让它始终站在你的 Schema 和业务规则之内作答。每次排查问题,都先怀疑 Schema 描述是否完整、示例是否把模型带偏了,而不是急着怪模型笨。养成这个调试习惯之后,一大半的玄学 bug 都能变成可复现、可修复的工程问题。
最后再分享一个小习惯:新环境里我永远先用一条最简单查询的 Few-shot 跑通全链路,在命令行里手动打印模型生成的 Cypher,确认没问题之后再逐步加示例、加复杂查询。别一上来就塞一大堆规则和示例,那样出了问题你根本不知道是 Prompt 的锅还是模型的锅。先简后繁,把链路每一环都验证扎实,这套方案离可用就不远了。