有人让我用大语言模型做"Driving directions",一开始我以为听错了——让LLM开车指路?后来想明白了,这里的关键其实不是"方向"本身,而是"memory"这个词。LLM怎么从它庞大的记忆里,把一条有效的、能落地的路线"提取"出来?这远比单纯让你给个聊天机器人写提示词有意思得多。就算你做的不是实际导航项目,这套"从模型记忆里导航"的思路,放到知识库问答、代码生成、文档梳理里也完全通用。
先说结论:想让LLM输出靠谱的驾驶路线,光靠模型自带的参数记忆是远远不够的。模型确实背过很多道路数据,但你问它"从某区到某大厦怎么走",它给你的很可能是一条看着通顺、实际上根本不存在的路——这就是典型的"语义通顺但事实错误"。解决思路是把它变成一套显式的记忆系统:短期记忆放着用户最近的意图和上下文,外部记忆放着真实的POI、道路拓扑和转向限制,结构化记忆管理路网图。LLM的角色从"记住了所有地图的活地图"变成"一个会看地图、会规划、会表达的向导"。这个转换看似简单,实际落地的时候坑非常多。
1. 从"背地图"到"查地图":LLM做导航的核心思路
1.1 LLM凭什么能输出路线?先搞清楚它记忆的是什么
先做一个简单的实验。你可以让任何一个主流的对话模型回答"从北京西站到颐和园怎么坐车",它基本都能给你一条大致可行的方案。这说明模型在训练阶段见过大量类似的对话文本,它记得住"从A到B通常会有几个步骤、大概经过哪些地标"。但要让它输出精确的、逐路口的驾驶指示,比如"沿某路向东行驶800米后右转进入某辅路",它就露馅了。原因在于:
- 道路数据的时效性:城市道路年年都在变,模型训练数据里根本没有最新的单行道、禁行、道路封闭信息。
- 坐标精度不足:模型记忆的是"文字描述"而不是"地理坐标",它知道"某大厦在某个商圈附近",但不知道具体经纬度,更不知道从哪个入口进停车场。
- 逻辑推理与空间计算的矛盾:转弯、变道、匝道选择,这是结合当前路况和道路拓扑的序列决策,不是"接话"能解决的。
所以开篇最核心的事情,就是把"记忆"重新分层。我在做的这个项目里就管它叫"三层记忆":工作记忆(当前对话与意图)、外部记忆(向量检索得到的真实POI和路线片段)、结构化记忆(可查询的路网图数据)。听起来有点学术,实际上就是把"人脑做导航"的过程拆成机械步骤——人也不是凭感觉走,而是先看手机地图,再听播报,最后结合自己当前位置做判断。LLM要做的事一模一样,只不过"看地图"这一步需要用检索来代替。
1.2 为什么不能直接微调一个"导航模型"
提一个很多新人会问的问题:既然LLM记忆不行,那我拿一批真实路线数据微调一个专用模型行不行?
我给你算一笔账:假设一个城市的有效道路段和转向关系是几十万条,加上POI信息,文本化之后大概是几千万到上亿个token。要微调出一个在这个城市范围内不幻觉的模型,光是高质量标注数据就需要覆盖所有道路组合,这个成本高得离谱。而且城市一变,模型立刻失效。
更重要的是逻辑层面的问题。导航是一个典型的"外挂知识强依赖"任务:路口的转向限制、单行线、施工绕行,这些不是语言规律,是外部事实。微调把外部事实塞进参数里,本质上是投机取巧,它并没有解决"如何访问和验证事实"这个核心问题。真正稳定的做法,是让模型知道自己不知道,然后在需要事实的时候去查外部记忆。这也是我始终推荐RAG(检索增强)架构做这类任务的原因。
1.3 项目整体架构:模型只做决策,记忆负责提供事实
我的方案可以概括成下面这个流程:
- 用户输入自然语言起点和终点。
- 一个轻量级的意图识别模块把文本标准化成"起点 + 终点 + 约束条件(避开高速/最短路径/不走小路)"。
- 用embedding模型把起终点和约束条件向量化,去外部记忆库(POI数据库 + 路网索引)里召回候选位置。
- 用一个图搜索算法(比如改进的A*算法)在结构化路网里算出一条符合约束的路径。
- 把这条路径转成人话描述,传给LLM,让它结合三层记忆信息生成最终的逐向驾驶指示。
- 输出前做一个合法性校验,用规则引擎检查生成的每一条指示是不是真的和路网数据吻合。
这里面有几个人容易忽略的点。一是"检索候选位置"这一步对exact match的要求很苛刻——用户说"去某医院",但POI库里可能有三家带类似名字的医院,你需要按城市、按距离、按用户历史常去地点做多级排序。二是"路径转人话"这一步,不能只给LLM一个坐标序列,它数不清第几个路口,你需要提前用道路名称和连接关系组织好。三是"合法性校验"重要性不亚于生成,因为LLM输出的"往东走200米"可能是错的,这个必须用真实坐标去复核,而不是靠再次让LLM自己检查。
这套架构下,LLM真正做的事情其实是"表达与选择":它从检索结果中挑选最合理的一个,把结构化的路径翻译成自然语言,并根据用户后面的追问做局部修正。背地图(参数记忆)的活,完全交给了显式的记忆系统。
2. 记忆系统怎么搭:向量库、路网数据、还有那些细节
2.1 外部记忆的类型化设计
我把"外部记忆"分成了三类,每一类在工程实现上差异很大:
- POI文本记忆:存的是地点的名称、别名、地址、经纬度、营业状态。适合用向量检索+n-gram召回。向量负责处理"某大厦""某广场"这种说法不一致的情况,n-gram负责精确匹配"某医院第一门诊部"这种专名。
- 路网结构化记忆:包含道路段、节点、转弯限制、道路等级。这部分不适合向量化,更适合用图数据库或者自定义的内存图结构。转弯限制尤其重要,现实中很多导航错误都是因为没考虑"这里不能左转"。
- 经验性路线记忆:历史导航记录、用户常走的路线、避开拥堵的经验片段。这部分可以用向量存,因为它是自然语言形式的,检索召回后可以作为参考给LLM。
单独把三类拆开来存,比一股脑全部向量化要高效得多。尤其是路网结构化记忆,如果强行embedding存储,你会发现检索的时候召回一堆坐标相近但根本不连通的路段,搜索算法根本没法用。
2.2 数据准备阶段踩过的坑
第一步自然是数据清洗。开放地图数据源拿到手之后,第一件要做的事不是导入向量库,而是做拓扑校验。我遇到的典型脏数据有:
- 道路段首尾不衔接,出现几米的空隙,导致路径搜索断掉。
- 转向限制信息缺失或错误,算法选了路却告诉你不能转。
- 同一路段在不同图层里名称不一致,"某大道"和"某大道辅路"被当成两条完全独立的路。
处理办法是写一套校验脚本,自动找断头路和名称漂移。断头路通过在距离阈值内合并节点解决,名称漂移则需要人工抽样核对。这一步不能省,省了后面所有环节都在为它买单。
POI数据也类似,最麻烦的点是简称和别名。我的经验是最少要维护一个"别名表",把用户最常说的口头叫法映射到官方名称。除此之外,POI的坐标偏移也需要处理——很多POI的坐标是建筑中心点,而不是入口,导航里直接生成"您已到达"就会误导用户。这个只能靠实地抽样校准或者引入更精细的AOI数据来缓解。
2.3 向量化和混合检索的实际参数
向量库我选择的是支持HNSW索引的那类常用方案。embedding模型选的是中文本地化效果比较好的那一档,512维,足够配合倒排索引做一个混合检索。
具体参数上,几个是实测调出来的:
- 向量检索top_k:我取50。太少了POI别名召回不全,太多了后续过滤开销大。
- 坐标距离过滤:对召回的50个候选先用haversine公式算实际距离,筛掉离目标超过3公里的结果,再做文本相关性排序。
- n-gram权重:精确匹配的权重比向量相似度高,因为驾驶需求里"某站""某医院"这种叫法必须精确。
- 多路结果融合:用RRF(倒数排名融合)合并向量检索和倒排索引的排序结果,效果比加权平均稳定。
很多人做RAG时喜欢只靠向量召回,这在普通文档问答里还行,在导航场景里是完全不够的——"某大厦"和"某大厦停车场入口"这种文本层面高度相似但语义位置截然不同的结果,只有靠地理约束才能排对。
3. 核心实现:从一条路径到一句人话
3.1 意图识别与起终点解析
用户说的话五花八门:"我要去某地""从家到某公园""导航到某广场北门"。这里我不让LLM直接解析,而是先用一个基于规则+轻量模型的解析器。规则模板负责常见的说法,模型兜底处理那些规则覆盖不了的长尾说法。
解析出来的结构大概是这样的:
{ "origin": {"name": "某小区", "coordinate": [116.31, 39.98]}, "destination": {"name": "某广场北门", "coordinate": [116.40, 39.92]}, "constraints": {"avoid_toll": True, "prefer_highway": False}, "current_location": [116.32, 39.97] }这里有一个非常关键的实操点:如果用户说"回家"或"去公司",就应该先查历史记忆里的常去地点,而不是走POI检索。这是经验性路线记忆的高价值应用场景。我在系统里加了一个"个人地标表",把高频目的地固化下来,配合用户在不同时段的位置,可以做出很自然的预测。
3.2 路网图搜索:A*算法的取舍
选路算法的时候,我一开始图省事用了Dijkstra,结果性能完全没问题,但"驾驶体验"很差——它会走出很多小路、烂路,因为代价函数里只有距离。后来换成改进的A*,代价函数改成:
cost = travel_time + road_level_penalty + turn_penalty + toll_penalty其中travel_time用路段的限速和长度估算,road_level_penalty按道路等级加权(高速、主干道权重低,小路权重高),turn_penalty给每次转弯加一个时间损耗,toll_penalty是避开收费路段的开关。
这几个权重的标定,用了一周多的时间反复试驾对比。最终比较合理的组合是:travel_time权重1.0,road_level_penalty小路系数加到0.25,turn_penalty每次转弯加15秒等效时间。这套参数在多数城市场景下出来的路线,更接近有经验司机会选的走法,而不是纯距离最短的走法。
A*的启发式函数我直接用haversine距离除以预估车速,简单但效率不错。节点数量控制在10万以内时,单次查询响应时间在50毫秒左右,完全够用。
3.3 把路径翻译成LLM能看懂的结构化文本
搜索得到一条路径之后,不能直接把节点序列丢给LLM。我把它转成了一种"导航操作序列":
1. 沿某大道东行 1.2 公里 2. 在某路口右转进入某路 3. 直行 800 米后靠右进入辅路 4. 在某立交桥第2出口驶出 5. 在目的地前方50米停车每个操作都附带起止坐标、涉及的道路名、行驶距离。LLM拿到这个结构化序列后,只需要做"润色"和"条件性补充",几乎没有自由发挥的空间。
反过来想,很多项目这里就出问题了——他们把原始坐标丢给LLM,让它"生成导航指示",LLM只能胡编。责任不完全在模型,而是你没有提供它可以用的事实约束。导航这个场景模型必须"照着讲",不能"自由讲"。
3.4 生成逐向指示的提示词要点
分享一个让我踩过不少次坑的地方:提示词的设计。核心原则是要求LLM执行三件事:
- 把结构化操作翻译成自然语言,保持每个操作的信息完整性——距离、方向、道路名都不能丢。
- 控制语气和长度:驾驶场景不适合啰嗦,每条指示尽量控制在15个词以内,但关键信息不能省。
- 对模糊处做标注,而不是靠上下文瞎猜。比如"在下一个路口右转"这种话,必须确保"下一个路口"在结构化数据里确实存在且只有一个。
我给LLM的提示词框架大概是:
你是一个精确的导航播报助手。下面是一段由系统生成的结构化路线数据。 请以驾驶者的视角,把它们改写为口语清晰、信息完整的逐向指示。 要求: 1. 不得添加任何结构化数据中不存在的路名、地标或转向。 2. 如果结构化数据中某段路没有道路名,请用"无名路"代替,不要自己编。 3. 距离按米或公里保留一位小数,不要四舍五入成整数以后丢失精度。 4. 每条指示之间用序号分隔。 5. 整体篇幅控制在原文长度的1.2倍以内。这个提示词看起来简单,但它的价值在于把模型的行为边界框住了。实测中,如果不加第1条和第2条,幻觉率会显著上升;如果把第5条去掉,LLM会把每条操作扩写成一段充满感受和周边描述的散文——看起来很专业,实际没法用。
3.5 合法性校验:最后一公里的保险
生成完之后,还有一个强制校验步骤。我写了一个独立的校验器,它逐条检查生成的指示:
- 每个指示中提到的道路名,是否出现在这条路径经过的实际路名集合里。
- 每条指示的方向词(东/南/西/北/左/右)是否与坐标差的方位一致。
- 距离是否在合理误差范围内(允许上下浮动15%)。
- 不存在"下个路口"这种指代但路径里前面没有路口可指代的情况。
这个校验器在早期救了我很多次。有一次测试,LLM把"沿某大道东行"写成了"沿某大道向东步行",就改了一个字,性质完全不同——驾驶和步行在路网里是完全不通的。校验器把这种问题直接拦下来了。
4. 调试实录:那些在测试路上现出的问题
4.1 起点终点解析不准:一个地址的多重身份
第一次上线测试,用户输入"去某商贸城",结果解析出来三个不同的POI,分布在城东、城西和城区中心。最开始我以为把top_k调大就能解决,后来发现这不是召回问题,而是排序问题。
解决方案是加了一个"城市内排序规则",按以下优先级过滤:
- 和用户当前真实位置的直线距离(来自设备GPS)。
- 是否在城市主城区范围内(排除同名郊区点)。
- 用户历史访问记录(如果存在)。
- 文本query和POI名称的精确相似度。
这三个规则组合之后,绝大多数情况都能锁定到唯一目标。如果还有两个都符合,就要在提示词阶段让LLM向用户确认,而不是自作主张选一个。驾驶导航里选错目的地是致命错误,宁可多问一句也不要猜。
4.2 LLM凭空造路:地图上没有的连接
调试中最头疼的问题就是幻觉。有一次测试从某古镇到某景区,LLM在中间一段凭空插入了"行驶至某乡道后转入某路",但实际上这两条路隔着一条河,中间没有桥。
这个问题的根源不在生成,而在提示词——模型觉得"路网应该连通",于是自己脑补了一条。封堵办法是前面说的合法性校验,但校验只能拦,不能改。后来我优化了prompt,加了一条:"如果你发现结构化路线数据中两段路之间的衔接看起来不合理,不要自行修改,标注'此处需要人工确认'。"这样至少把问题暴露出来,而不是让幻觉悄悄溜进结果里。
4.3 距离数字总差一点:坐标系的统一问题
中途还遇到过一个隐蔽的bug:生成的距离和实际行驶距离总是差5%到8%。查了很久才发现是坐标精度问题——一部分POI的数据源是WGS84坐标,另一部分是国测局加密坐标,两者混用导致距离计算系统性偏差。
解决办法是在数据入库之前做统一的坐标转换,全部转换到同一个坐标系。这个没什么高深技巧,完全看数据治理细不细心,但它是所有后续步骤的地基。
4.4 上下文太长了怎么办:压缩路线描述
有一版方案想把完整的路线段描述全部塞进LLM提示词里,结果一条30公里的跨城路线生成了上千tokens的结构化数据,不仅浪费,还导致LLM开始"忘事"——它的注意力会被无关路段描述稀释。
后来我做了"分段生成":把路线切成若干段,每段包含的转弯操作不超过8个。LLM逐段生成后拼起来,再统一做一致性校验。分段之后,每条指示的质量明显提升,删字数和错觉类的错误大幅减少。这也算一个通用的经验:LLM不是内存越大越好,恰当的信息裁剪比堆context更重要。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查和解决 |
|---|---|---|
| 起点终点解析成多个结果 | POI重名或简称歧义 | 增加当前位置距离过滤,按用户历史优先排序,必要时反问确认 |
| 路线里出现地图上不存在的路 | 模型幻觉 | 合法性校验拦截,提示词约束禁止自行补路 |
| 距离差5%以上 | 坐标系不统一 | 统一数据源的坐标转换,偏差率作为数据质检指标 |
| 生成指示啰嗦且丢关键信息 | Staff_prompt缺少结构和长度约束 | 缩短单次生成路段,强制使用结构化路线数据 |
| 搜索慢,超过1秒 | HNSW参数不合适或候选集过大 | 调整M/efSearch参数,增加距离预过滤,缩小检索范围 |
| 用户反馈"绕路" | 权重参数没标定 | 收集真实路线的偏好样本,重新计算道路等级惩罚系数 |
| 输出里出现"无法回答" | 检索为空且无兜底策略 | 配置基础兜底模板,引导用户提供地标或交叉路口 |
5. 一些可以复用的经验
这套"从LLM记忆里导航"的架构,最后跑通了,效果稳定到可以在城市级范围输出可用的驾驶指示。但比结果更重要的是几个心得体会:
第一,不要试图让模型记住事实。把记忆外置,模型只做表达和决策,这是稳定性的根本来源。很多人一上来就微调一个导航大模型,最终都会撞上数据时效性和成本这两堵墙。
第二,中间的校验层永远不能省。LLM生成的自然语言天然有"流利性优先"的倾向,而驾驶指令是"事实性优先"的任务,没有校验层就是拿命开车。
第三,提示词对模型行为的约束能力,远远超过常规认知。不是给模型写一段好话就行,而是明确告诉它什么不能做、遇到异常怎么处理、哪些边界不能碰。
第四,任何数据在进入记忆系统之前,都必须经过拓扑和坐标系级别的质检。这是最不酷但性价比最高的一件事。脏数据直接决定了上层效果的天花板,你再怎么调prompt和参数,都跳不出数据质量的限制。
这个项目的价值不只在导航本身。回头想想,所有需要LLM基于真实世界数据提供决策建议的场景——比如医疗导诊、法律咨询、设备维修指引——本质上都是"从LLM记忆里给出方向"。记忆系统的设计、检索策略的编排、提示词边界的约束、合法性校验的兜底,这套方法论是可以整体平移过去的。至少对我来说,做完这个项目之后,再遇到任何"让LLM输出可信事实"的问题,我都不会再第一时间想到微调,而是先想:它的记忆在哪里,记忆够不够新,够不够准。