Skip to content
Joker.Yun
Go back

最近看到一个挺有意思的 AI 旅游项目:把 TripStar 拆了拆

Updated:
约 15 分钟读完Edit page

最近看到一个挺有意思的开源项目 TripStar(旅途星辰,一个 AI 文旅智能体)——输入城市、日期、偏好,它就能自动生成一份完整旅游攻略。我把它从架构到源码拆了一遍,记录在这篇文章里。

简单说,它要解决两个挺典型的问题:一是数据从哪来——高质量攻略都散在社交平台的游记里,而不是官方宣传;二是大模型吐出来的 JSON,凭什么敢直接渲染成行程。这两个问题的答案,占了它源码里最有意思的部分。

一、功能全貌:它到底做了什么

传统攻略站的问题是信息过载,TripStar 的解法是”像人类旅行管家一样”把信息搜齐再规划——底层基于 HelloAgents 框架的多智能体协作。它的能力清单:

二、架构总览:三层分离 + 异步任务

项目采用标准的前后端分离:

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 的方案:

  1. POST /api/trip/plan 立即返回 task_id 和 ws_url,推理任务丢进后台 asyncio.create_task;
  2. 客户端通过 WebSocket 订阅 /ws/{task_id},实时接收 processing / progress 状态(前端看到的”🔍 正在搜索景点…”就是这么来的);
  3. 任务状态持久化到磁盘,历史计划可查。

翻开源码看,规划主流程是逐城市推进的:每个城市依次执行”景点搜索 → 天气查询 → 酒店搜索”,所有同步调用都包在 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)

三、多智能体协作:主控 + 专员

规划阶段是经典的分工协作:

三者搜集完,统一注入终极 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)的每个请求都要携带一组签名:

这些由前端一段被 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 数据流:搜索 → 详情 → 提纯 → 复核

  1. 搜索:POST /api/sns/web/v1/search/notes,搜”西安 必去”这类关键词,拿到笔记卡片列表;
  2. 取详情:每条笔记附 note_id 与动态令牌 xsec_token(必须原样透传),向 /api/sns/web/v1/feed 换取正文;
  3. 降级方案:原生 API 失败时,退化为直接抓 www.xiaohongshu.com/explore/{note_id} 网页源码,从 window.__INITIAL_STATE__ 提取同一份数据——双引擎互为备份;
  4. LLM 提纯:聚合游记文本交给 LLM,提纯为结构化景点(名称/描述/游玩时长/是否需要预约);
  5. 坐标复核:提纯出的坐标不直接采信,走高德地理编码反查核实,并对高德调用做了限速排队(免费接口有 QPS 限制)。
  6. 景点配图:前端按景点名独立调 /api/poi/photo,后端再去小红书搜”xxx 风景”拿到笔记首图直链——图片也从小红书实时来。

五、核心链路②:双地图引擎与降级哲学

国内访问 Google 服务需要代理,这对”国内为主”的应用是个硬约束。TripStar 的设计:

“每一条外部依赖旁边都站着一个备胎”——这是它贯穿全项目的工程哲学。

六、核心链路③: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 输出”的正确姿势:防御纵深,而不是单点祈祷。

七、工程细节集

八、代价与边界

冷静地说,这套东西不是没有代价:

环节代价
小红书链路签名算法带版本迭代,社区长期军备竞赛;依赖登录 Cookie;频率受限,账号/IP 有封禁风险;在平台条款与法律上属灰色地带
Google 引擎需要绑卡与代理,国内开箱即用性差
复杂 JSON五级容错背后是”LLM 输出不可靠”这一无法根除的事实
部署高德双 Key、多服务商 Key,配置门槛不低

九、值得带走的设计

  1. 数据源选择:真实 UGC > 官方宣传。“小红书提纯”当核心卖点,方向判断很准;
  2. “复用”优于”重写”:面对混淆且频繁升级的算法,保留算法本体、黑盒调用——把不可维护的东西隔离在边界外;
  3. 防御纵深:从五级 JSON 容错到”每个外部依赖都配备胎”,全项目贯穿”假定失败”的思维;
  4. 对 LLM 的克制:LLM 负责提纯与规划,但坐标这种硬事实必须走高德复核——该用模型的地方用模型,该用确定性系统的地方绝不含糊;
  5. 工程判断高于技术炫技:什么数据值得爬、什么服务要备胎、什么输出要兜底,每个决策都算过成本收益。

结语

TripStar 的 README 里有一句话很实在——它解决的是”信息过载”和”决策疲劳”。而拆完源码你会发现,它真正解决的其实是工程中最经典的两类问题:如何从不友好的数据源里稳定取数,以及如何与一个不可靠的生成系统安全共事。

项目地址:github.com/1sdv/TripStar。本文拆解仅用于技术学习交流;若你要自己动手,请先评估合规与风控风险。


Edit page
Share this post:

Previous Post
Piper 机械臂 + LinkerHand 灵巧手:21 个实战踩坑记录(IK / 标定 / 通信 / 仿真)
Next Post
牛客出了个官方求职 MCP:一个校招信息收集的新通道