最近看到一个挺有意思的开源项目 TripStar(旅途星辰,一个 AI 文旅智能体)——输入城市、日期、偏好,它就能自动生成一份完整旅游攻略。我把它从架构到源码拆了一遍,记录在这篇文章里。
简单说,它要解决两个挺典型的问题:一是数据从哪来——高质量攻略都散在社交平台的游记里,而不是官方宣传;二是大模型吐出来的 JSON,凭什么敢直接渲染成行程。这两个问题的答案,占了它源码里最有意思的部分。
一、功能全貌:它到底做了什么
传统攻略站的问题是信息过载,TripStar 的解法是”像人类旅行管家一样”把信息搜齐再规划——底层基于 HelloAgents 框架的多智能体协作。它的能力清单:
- 一键行程:填地点/日期/偏好(交通方式、住宿风格、旅行兴趣、特殊需求),坐等一份完整攻略;
- 小红书深度集成:景点推荐与攻略数据来自小红书真实用户游记,LLM 提纯出避坑指南与打卡建议;景点配图也是从小红书实时搜的”网友实拍”;
- 景点预约提醒:从游记里识别需要提前预约的景点(故宫、陕西历史博物馆这类),在行程卡片里醒目标注——防止白跑一趟;
- 双地图引擎:Google Maps 与高德地图无缝切换、自动回退(国外用 Google,国内回退高德),按真实经纬度绘制”起点-景点-终点”路线;
- 精准预算面板:自动汇总门票、餐饮、住宿、交通多维花销;
- 知识图谱可视化:把行程转成”城市-天数-行程节点-预算”的节点关系图;
- 沉浸式伴游问答:行程生成后,可随时追问详情,AI 持有完整行程上下文;
- 多城市行程:一次旅行规划多个城市,城际移动日标注交通建议,预算独立统计城际费用;
- 用户偏好记忆:分权重的偏好库,支持遗忘机制与 TOP-K 召回,持续让推荐贴合用户习惯。
二、架构总览:三层分离 + 异步任务
项目采用标准的前后端分离:
frontend/ Vue 3 交互层(Home 表单输入 / Result 路书展示)
backend/ FastAPI 服务层(api/routes: trip, poi, chat)
agents/ 多智能体定义与编排(trip_planner_agent.py)
services/ xhs_service / llm_service / knowledge_graph_service
models/ Pydantic schemas
真正值得学的是它对”超长任务”的处理。LLM 生成一份完整行程要几分钟,同步 HTTP 请求必然 504 网关超时。TripStar 的方案:
POST /api/trip/plan立即返回task_id和ws_url,推理任务丢进后台asyncio.create_task;- 客户端通过 WebSocket 订阅
/ws/{task_id},实时接收processing / progress状态(前端看到的”🔍 正在搜索景点…”就是这么来的); - 任务状态持久化到磁盘,历史计划可查。
翻开源码看,规划主流程是逐城市推进的:每个城市依次执行”景点搜索 → 天气查询 → 酒店搜索”,所有同步调用都包在 asyncio.to_thread 里避免阻塞事件循环,进度则按城市折算成百分比节点:
progress_base = int(10 + (idx / total_cities) * 65) # 每个城市分摊 10~75 之间的进度
await self._emit_progress(progress_callback, "attraction_search",
f"正在搜索 {city} 的景点...{city_label}", progress_base)
三、多智能体协作:主控 + 专员
规划阶段是经典的分工协作:
- 主控 Agent(MultiAgentTripPlanner):接收请求、拆解任务、统筹最终规划;
- 天气管家:查询目标日期气候;
- 酒店专员:根据预算与住宿风格找落脚点;
- 小红书专员:搜景点、去伪存真、补齐坐标(后文详拆)。
三者搜集完,统一注入终极 Planner Prompt,由主模型输出包含行程、预算、预约提醒、防坑指南的复杂嵌套 JSON。
天气链路里有个很”野战”的细节:Google 天气失败时,代码用一组关键词做启发式降级判据——
_weather_fail_keywords = ("无法", "失败", "错误", "error", "unknown", "抱歉", "sorry")
if self.map_provider == "google" and any(kw in weather_response.lower() for kw in _weather_fail_keywords):
weather_response = await self._fallback_amap_weather(city) # 降级到高德天气 REST
土吗?土。但在 LLM 输出没有结构化错误码的现实里,这种”暗号识别 + 果断降级”反而最实用。
四、核心链路①:小红书数据是怎么打通
这是整个项目最有技术含量的部分。小红书对第三方没有任何开放 API,反爬工程出了名的卷。TripStar 的完整链路:
4.1 拦路虎:签名头
小红书网页后端(edith.xiaohongshu.com)的每个请求都要携带一组签名:
x-s:核心请求签名(由请求路径、请求体、时间戳计算)x-s-common:环境指纹类签名x-t/x-b3-traceid/x-xray-traceid:时间戳与风控链路追踪
这些由前端一段被 xray 强力混淆的 JS 生成,算法带版本号且频繁升级。
4.2 解法:不重写算法,让算法自己跑
项目目录里躺着几份从网页端扒下来的混淆 JS(xhs_xs_xsc_56.js——“56”就是算法版本号),然后 sign_util.py 用 PyExecJS 起本地 Node.js 运行时直接执行它们,把签名当黑盒调用:
def generate_xs_xs_common(a1: str, api: str, data="", method="POST"):
"""调用签名 JS 生成 x-s, x-t, x-s-common"""
ret = _xs_js.call("get_request_headers_params", api, data, a1, method)
return ret["xs"], ret["xt"], ret["xs_common"]
签名输入里的 a1 是登录 Cookie 中的设备指纹字段。核心洞察:混淆 JS 的价值是”能算出正确结果”,而不是”被读懂”——算法会变,那就保留算法本体、每次只换输入,维护成本从”重新逆向”降级为”换一份 JS”。
4.3 数据流:搜索 → 详情 → 提纯 → 复核
- 搜索:
POST /api/sns/web/v1/search/notes,搜”西安 必去”这类关键词,拿到笔记卡片列表; - 取详情:每条笔记附
note_id与动态令牌xsec_token(必须原样透传),向/api/sns/web/v1/feed换取正文; - 降级方案:原生 API 失败时,退化为直接抓
www.xiaohongshu.com/explore/{note_id}网页源码,从window.__INITIAL_STATE__提取同一份数据——双引擎互为备份; - LLM 提纯:聚合游记文本交给 LLM,提纯为结构化景点(名称/描述/游玩时长/是否需要预约);
- 坐标复核:提纯出的坐标不直接采信,走高德地理编码反查核实,并对高德调用做了限速排队(免费接口有 QPS 限制)。
- 景点配图:前端按景点名独立调
/api/poi/photo,后端再去小红书搜”xxx 风景”拿到笔记首图直链——图片也从小红书实时来。
五、核心链路②:双地图引擎与降级哲学
国内访问 Google 服务需要代理,这对”国内为主”的应用是个硬约束。TripStar 的设计:
- 后端:
GOOGLE_MAPS_PROXY只作用于 Google Maps 服务,不污染 LLM/小红书/高德的请求; - 回退:地理编码”Google 优先,失败降级高德 REST”;天气查询同样”Google 失败 → 高德天气”;
- 前端:高德 JS API 2.0 渲染,读取 POI 经纬度动态绘制连线与标记;安全密钥 JSCode 通过构建期变量注入,避免手写进
index.html。
“每一条外部依赖旁边都站着一个备胎”——这是它贯穿全项目的工程哲学。
六、核心链路③:LLM 的四个角色与五级 JSON 容错
让 LLM 输出”包含行程、预算、预约提醒的复杂嵌套 JSON”,作者自己都在架构图里标了**“【高危操作】”**。TripStar 的应对是分工 + 兜底:
LLM 的四个角色:①从游记提纯景点;②主规划(排行程);③伴游问答;④修复 JSON。
五级 JSON 容错解析(_parse_response,源码级流程):
# 提取:优先 ```json 代码块 → ``` 块 → 首尾大括号(截断时取到末尾)
parse_attempts = [
("基础清理", json_str), # ① 清理杂乱字符
("修复未转义引号", fixed_quotes), # ② 修复未转义引号
("截断修复", repaired), # ③ 补齐括号(输出被截断场景)
("截断+引号修复", repaired_fixed), # ④ 组合修复
("正则提取", brutal), # ⑤ 正则暴力提取
("正则+截断修复", brutal_repaired), # ⑥ 再组合
]
# ……全部失败 → 求助 LLM 修复(_llm_repair_json)
# 再失败 → 抛出异常(外层还有 _create_fallback_plan 备用计划)
逐级尝试、每级打印失败上下文;本地修复全挂后,才把”修复”这件脏活交给 LLM 本身。这是”不信任 LLM 输出”的正确姿势:防御纵深,而不是单点祈祷。
七、工程细节集
- 用户偏好记忆:
ENABLE_USER_MEMORY=true时,从MemoryManager按 user_id 取偏好片段(build_prompt_snippet),注入规划 Prompt——记忆不是摆设,是真的进上下文;支持权重、遗忘与 TOP-K 召回; - 历史计划持久化:任务状态落盘,前端可查历史;
- 配置运行时修改:前端设置页可改后端配置,敏感字段遮罩返回,保存遮罩值时后端保持原密钥不变——小细节,却是很多项目会翻车的地方;
- 前端懒加载搜图:行程渲染后逐景点异步搜图,不阻塞主流程。
八、代价与边界
冷静地说,这套东西不是没有代价:
| 环节 | 代价 |
|---|---|
| 小红书链路 | 签名算法带版本迭代,社区长期军备竞赛;依赖登录 Cookie;频率受限,账号/IP 有封禁风险;在平台条款与法律上属灰色地带 |
| Google 引擎 | 需要绑卡与代理,国内开箱即用性差 |
| 复杂 JSON | 五级容错背后是”LLM 输出不可靠”这一无法根除的事实 |
| 部署 | 高德双 Key、多服务商 Key,配置门槛不低 |
九、值得带走的设计
- 数据源选择:真实 UGC > 官方宣传。“小红书提纯”当核心卖点,方向判断很准;
- “复用”优于”重写”:面对混淆且频繁升级的算法,保留算法本体、黑盒调用——把不可维护的东西隔离在边界外;
- 防御纵深:从五级 JSON 容错到”每个外部依赖都配备胎”,全项目贯穿”假定失败”的思维;
- 对 LLM 的克制:LLM 负责提纯与规划,但坐标这种硬事实必须走高德复核——该用模型的地方用模型,该用确定性系统的地方绝不含糊;
- 工程判断高于技术炫技:什么数据值得爬、什么服务要备胎、什么输出要兜底,每个决策都算过成本收益。
结语
TripStar 的 README 里有一句话很实在——它解决的是”信息过载”和”决策疲劳”。而拆完源码你会发现,它真正解决的其实是工程中最经典的两类问题:如何从不友好的数据源里稳定取数,以及如何与一个不可靠的生成系统安全共事。
项目地址:github.com/1sdv/TripStar。本文拆解仅用于技术学习交流;若你要自己动手,请先评估合规与风控风险。