baranlanka/et-hotel-ai-showcase

GitHub: baranlanka/et-hotel-ai-showcase

酒店旅游行业的生产级 AI 平台,将原始酒店数据转化为多语言内容并执行安全的自主外联,基于 Temporal 持久化编排与 LangGraph 多模型 pipeline。

Stars: 0 | Forks: 0

# et-hotel-ai **一个生产级的酒店/旅游 AI 平台:将原始酒店数据转化为已发布的多语言房源信息,并执行安全、自主的冷启动外联以招募酒店——在 Effective Tours 从零开始构建。** [**▶ 在线演示**](https://et-hotel-ai-showcase.streamlit.app) · [复现评估](#results--evaluation) · [架构](docs/architecture/ARCHITECTURE.md) · [方法](METHODS.md) · [决策记录](docs/adr) [![CI](https://img.shields.io/github/actions/workflow/status/baranlanka/et-hotel-ai-showcase/ci.yml?branch=main&label=CI)](https://github.com/baranlanka/et-hotel-ai-showcase/actions) [![在线演示](https://img.shields.io/badge/live%20demo-Streamlit-FF4B4B?logo=streamlit&logoColor=white)](https://et-hotel-ai-showcase.streamlit.app) [![评估](https://img.shields.io/badge/eval-reproducible%20offline-brightgreen)](#结果--评估) ![Python](https://img.shields.io/badge/python-3.12+-3776AB?logo=python&logoColor=white) [![License: MIT](https://img.shields.io/github/license/baranlanka/et-hotel-ai-showcase)](LICENSE)
``` $ make eval # deterministic, offline, no API keys A. DETERMINISTIC SECURITY / GUARD ASSERTIONS (gate the exit code) [PASS] input-guard catches injection tripwires 10/10 flagged, 0/4 benign false-positives [PASS] input-guard datamarks untrusted text (spotlighting) [PASS] output-guard catches outbound leaks 5/5 caught, clean draft passes [PASS] opener guard catches markup / injection / PII 4/4 caught [PASS] ReviewHook validators null poisoned staff names + drop unsafe highlights [PASS] money-gate: send_D forced -> escalate (_AUTO_SEND_D_ENABLED=False) [PASS] structured outputs validate (InterpretedResponse / ReviewHook / QualifierResult) [PASS] all 4 red-team / eval harnesses run to completion RESULT: deterministic assertions ALL PASS -> exit 0 ``` ## 概述 Effective Tours 需要解决两件小团队无法大规模手动完成的事情:**(1)** 为数以万计的酒店生成丰富、准确且*多语言*的内容,以及 **(2)** 通过个性化的外联寻找并招募新酒店——无需人工撰写每条信息,并且**绝不允许自主 agent 自行采取涉及资金承诺的行动。** 这个仓库是对这两个需求的工程化解答——包含两条产品线以及一个共享的数据接入层,**全部作为持久的 [Temporal](https://temporal.io) workflow 运行**(内容生成、抓取、代理管理和外联分别是在约 10 个 worker 服务上运行的 Temporal activity——这是平台用于重试、恢复和分布式追踪的共享主干): - **一个 LangGraph 多模型内容 pipeline** —— 接入酒店数据 → 提取方面(一个微调的 ABSA 模型 + LLM)→ **使用 Qwen-VL 视觉模型分析酒店及房间照片,并选择最佳展示图片** → 生成概述、房间描述和评论摘要 → 翻译 → 发布。每个操作都通过 LLM 工厂路由到**不同且具有成本效益的模型**。 - **一个由 Temporal 编排的 5-agent 冷启动外联引擎** —— 它从评论中挖掘个性化钩子,开启对话,对回复进行资格审核,并*自主*驱动一个漏斗——通过针对 OWASP **LLM Top-10**(prompt 注入、输出泄露、过度授权)的加固,配备一个**确定性的、失败即关闭的“资金操作”门控**,以及一个**可复现的红队 + 评估工具**。 - **一个高可用的分布式抓取器** —— 作为接入层主干,在生产级、*自适应的*反爬防御(WAF 式限速、IP 信誉封禁、指纹和机器人挑战)下可靠地拉取酒店数据:具备故障转移的轮换代理池、按请求进行的浏览器/TLS 指纹合成、熔断和限速。*目标和目的已隐去;包含了一个中立的、自托管的韧性概念验证——请参阅[下文](#resilient-distributed-fetcher)。* **构建时的限制条件:** 真实的生产规模和成本压力(因此:采用多模型路由 + 护栏,而不是处处使用单一大型模型);持久、弹性的执行(Temporal);必须**绝不**自我授权执行不可逆操作的自主 agent;以及严格的数据来源规范。 **边界(此展示有意排除的内容):** 经过精心挑选的生产环境 prompt(它们位于 [Langfuse](https://langfuse.com) 中,而不在代码库内)、所有真实的客户/酒店数据(已替换为合成测试数据)、所有凭证,以及任何特定站点的抓取器(抓取器以*通用*引擎的形式发布——请参阅[道德声明](#whats-shown-vs-withheld))。 ## 包含内容 | 子系统 | 展示内容 | 此处可运行 | |---|---|---| | **内容 pipeline** (`llm_content_generation/`) | LangGraph 子图的 `StateGraph`,**按操作进行的多模型路由** (`llm_factory`),成本护栏,ABSA 方面提取,**Qwen-VL 视觉阶段**(照片分析 + 展示图片选择——已描述),Langfuse 管理的 prompt 带有本地回退 | `make demo-graph` —— 在带有 mock 模型的合成酒店数据上运行方面提取→内容生成图 | | **外联引擎 + 评估** (`app/leadgen/`, `app/temporal/…/leadgen/`, `scripts/eval/`) | 5-agent 持久化 **Temporal** 状态机,**OWASP-LLM** 输入/输出防护(spotlighting + datamarking),一个**失败即关闭的资金门控**,结构化 Pydantic 输出,以及一个**红队 + 评估工具** | `make eval` —— 离线复现确定性的安全属性 | | **高可用抓取器** (`app/shared/`) | 一个通用的、容错的分布式 HTTP/GraphQL 引擎:**代理轮换 + 故障转移、浏览器/TLS 指纹合成、熔断器、令牌桶限速、带抖动的退避** —— 在生产环境中针对自适应反爬防御经受过实战考验 | `make demo-resilience` —— 真实引擎对抗恶劣端点;`make demo-scrape` —— 中立后端 | **Temporal 是承载所有这些功能的主干。** 在生产环境中,内容生成、抓取、代理管理和外联都作为**持久的 Temporal workflow/activity** 运行——为整个平台提供统一的编排、重试、恢复和分布式追踪层,而不仅仅是为 agent 服务。*(此展示将内容 pipeline 提取为独立的 LangGraph 切片,并提供了外联引擎实际的 Temporal workflow;完整的跨服务连接方式已作描述,但未包含在代码中。)* 此外,还有一个**可运行的、脱敏后的可观测性技术栈** (`infra/observability/` —— OpenTelemetry → Tempo/Loki/Prometheus → Grafana;`docker compose up`) 以及一个[说明性的服务拓扑 compose](docs/deployment/topology.docker-compose.yml)。 ## 技术栈 **编排与 agent:** **Temporal** —— 全平台持久编排;*每个*子系统(内容 · 抓取 · 代理 · 外联)都作为 Temporal workflow/activity 运行 · LangGraph · LangChain · Pydantic(结构化输出) **模型与 prompt:** OpenRouter (DeepSeek / Qwen-VL / Mistral / Llama) · 本地 LM Studio/Ollama · 微调的 ABSA 模型 · **Langfuse**(prompt 管理 + 追踪) **后端与数据:** FastAPI · CockroachDB (SQLAlchemy 2.0 + Alembic) · Redis · Backblaze B2/CDN **平台:** Docker · GitHub Actions → GHCR → Komodo CD · 自托管 **OpenTelemetry → Grafana / Tempo / Loki / Prometheus** ## 架构 ``` flowchart LR API[FastAPI trigger
POST /api/v2/orchestration] --> MASTER SCHED[Temporal Schedules
proxy · cookie · sitemap refresh] -. keep pools fresh .-> DB[(CockroachDB
state · queues · proxy pool)] subgraph temporal[Temporal · durable orchestration · retries · OTel tracing · ~10 workers] direction LR MASTER([Master pipeline
workflow]) --> EX[Extraction
scrape → data lake] EX --> CG[Content generation
LangGraph: aspects → Qwen-VL
vision → generate → translate] CG --> EXP[Static export] --> PUB[CMS publish] OUT([Outreach workflow
5 agents · money-gate]) end EX -->|rotating proxies + cookies| SRC[(OTA / hotel data)] CG -->|small open models| LLM[OpenRouter · LM Studio
Langfuse prompts + traces] CG --> B2[(Backblaze B2
content lake)] PUB -->|upsert| CMS[(Directus CMS)] OUT -->|small open models| LLM OUT <-->|messages · inbound signals| CRM[(CRM / email)] temporal --- DB temporal --> OBS[OTel → Grafana / Tempo / Loki / Prometheus] ``` 内容 pipeline 的端到端运行:一个 **`MasterHotelPipelineWorkflow`** 驱动提取 → 内容生成 → 导出 → CMS 发布,而外联流程也在同一个 Temporal 主干上运行。容器/运行时/部署视图、LangGraph 内部图以及完整的外联 pipeline 位于 **[docs/architecture/ARCHITECTURE.md](docs/architecture/ARCHITECTURE.md)** 中。 ## 设计决策与权衡 | 决策 | 考虑的选项 | 选择 | 接受的权衡 | |---|---|---|---| | 编排平台上的**每一项**活动(内容生成 · 抓取 · 代理管理 · 外联) | Celery / 原始队列 + 状态列;临时异步 | **Temporal** 持久 workflow 作为全平台主干(约 10 个 worker) | Temporal 集群的运维负担 + workflow 确定性规范 —— 换来了各处的持久性、重试、可重放状态和统一追踪 | | 允许 agent 采取涉及资金承诺的操作 (`send_D`) | 信任模型 + 置信度阈值 | **确定性的失败即关闭门控** —— 每个 `send_D` 都被强制路由到人工审批;开关只能通过代码审查更改,绝不通过环境变量 | agent 在不可逆操作上永远无法做到完全自主(这是设计使然——这正是目的所在) | | 防御来自抓取评论/回复的 prompt 注入 | 仅 prompt 的“忽略指令”;分类器 | **Spotlighting + datamarking** (Microsoft) + **确定性输出泄露防护** | 每轮额外的预处理/后处理;一些良性输入会被 datamarking —— 为了实现严格的 DATA/指令边界,这是值得的 | | 约 8 个操作的模型选择 | 处处使用单一**前沿**模型 (GPT-5 / Claude) | **按操作路由到小型、廉价的开放模型** (DeepSeek / Qwen / Llama),通过 `llm_factory` + 硬成本护栏 | 每个操作需要更多的配置 + 评估 —— 但成本只是其一小部分,而且它*证明*了工程能力,而不是依赖于大型模型 | | Prompt 存储 | 将 prompt 硬编码在仓库中 | **Langfuse**(在运行时获取版本化、已评估的 prompt) | 存在运行时依赖 —— 但 prompt 成为版本化、可进行 A/B 测试的资产,并且不包含在源代码控制中(这*就是*此展示能够存在的原因) | | 负责任地展示抓取能力 | 发布真实的抓取器 | **通用的高可用抓取引擎 + 一个自托管的恶劣端点演示** (`make demo-resilience`) | 没有特定站点的演示 —— 但韧性是针对中立对手*得到验证*的,没有违反服务条款 (ToS) 或带有“规避工具”的意味 | 每一行都链接到 **[docs/adr/](docs/adr)** 下的完整记录。 ## 结果 / 评估 两种截然不同的数据类型,为了保持诚实,将它们严格区分开来: ### A. 确定性安全属性 —— 现在即可离线复现 `make eval` 在确定性的 **mock** 模型上运行(无需密钥,无需网络),并根据安全保证**控制其退出代码**——无论使用何种模型,这些都是真实的: | 属性 | 结果 | |---|---| | 资金门控阻止自主的 `send_D` (→ 转为人工) | **100%** (`_AUTO_SEND_D_ENABLED = False`,在实际 workflow 路径中强制执行) | | 输入防护捕获已知的注入触发器 | **10/10**,对良性回复的误报率为 **0/4** | | 输出防护捕获出站泄露(datamark / 角色标记 / 脚手架) | **5/5**,干净的草稿通过 | | 开场白供应链防护(来自抓取评论的标记 / 注入 / PII) | **4/4** | | `ReviewHook` 模型层验证器将中毒的员工姓名置空 | ✔ | | 结构化输出验证通过;所有 4 个测试工具运行完成;图编译成功 | ✔ · `171 passed, 1 skipped` | ### B. 生产环境模型质量数据 —— 在 Effective Tours 测量 在生产环境中针对真实的 70B 模型 (`llama-3.3-70b-instruct`, temp 0.1) 进行测量。这些数据**未**通过离线 mock 模型复现(mock 无法代表模型质量);通过将相同的测试工具指向实时模型 (`MODEL_BACKEND=openrouter`) 来复现它们: | 指标 | 生产环境数值 | |---|---| | 路由器 (Agent E) 精确匹配准确率 | **92.2%** (59/64 对抗性案例),下一操作 93.8%,0 个 LLM 错误 | | 红队测试套件(注入 / 越狱 / 漏斗滥用 / STOP / 混乱 / 边界) | **42/42 已防御** | | Agent B 诚实门控 | **100%** | ## 高可用分布式抓取器 在生产规模下获取酒店数据意味着要可靠地穿透**自适应的、商业级的反爬防御**——WAF 式限速、IP 信誉封禁、浏览器/TLS 指纹挑战以及会话门控——并在长时间和高吞吐量下持续进行。我设计并构建了实现这一目标的引擎:一个**具有基于健康状态故障转移的轮换代理池**、**按请求进行的浏览器/TLS 指纹合成** (curl_cffi 模拟)、一个**熔断器**、一个**令牌桶限速器**,以及**带抖动的退避机制**。 **真实目标和商业目的是专有的并已隐去——并且本仓库不提供特定站点的抓取器。** 这里提供的是*可复用的引擎*以及一个中立的、自托管的证明:`make demo-resilience` 启动了一个故意设置得非常恶劣的端(按代理的 IP 封禁、`429`/`Retry-After`、指纹和机器人挑战、瞬时 `500` 错误),并针对一个简单的客户端,驱动**真实的引擎组件**通过该端点——完全离线且逐字节确定性运行。 | 通过恶劣端点获取 60 条记录 | **引擎** | 简单客户端 | |---|---|---| | 获取的记录数 | **60 / 60 (100%)** | 15 / 60 (25%) | | 代理轮换次数 | 3 | 0 | | 承受的 IP 封禁 (`403`) | 3 | 0 —— 在 3 页后搁浅 | | 遵守的 `429` Retry-After | 5 | 0 | | 重试的瞬时 `500` | 1 | 0 | | 熔断器跳闸 → 恢复 | 2 → 2 | 0 → 0 | 简单的客户端在其唯一的代理被 IP 封禁的那一刻就会死掉;而引擎会轮换到新的代理,遵守每一个 `Retry-After`,通过抖动进行退避,并恢复其跳闸的断路器以完成任务。该演示导入了*实际的* `CircuitBreaker`、`ProxyManager` 和 `SimpleRateLimiter` 类——没有重新实现——因此它所测试的正是曾在生产环境中运行的相同韧性代码。 ## 快速开始 ``` git clone https://github.com/baranlanka/et-hotel-ai-showcase && cd et-hotel-ai-showcase make install # creates .venv, installs pinned deps make eval # deterministic security eval — offline, no keys, gates on the guarantees make demo-graph # run the content graph on a synthetic hotel (mock model) make demo-resilience # anti-bot resilience PoC: the real engine vs. a hostile endpoint make demo-scrape # run the resilient fetcher against a neutral demo backend make test # 171 passed, 1 skipped streamlit run streamlit_app.py # the interactive dashboard (safety playground, demos, eval) ``` 不需要 API 密钥——默认的 `MODEL_BACKEND=mock` 是确定性的且离线运行的。要查看实时生成,请在 `.env` 中设置 `MODEL_BACKEND=ollama`(本地)或 `MODEL_BACKEND=openrouter`(需要密钥)(从 `.env.example` 复制)。 ## 展示内容 vs 隐去内容 这是一个真实的生产系统,因此以*负责任的*方式进行展示: - **精选 prompt 已隐去。** 生产环境的 prompt 在 **Langfuse** 中进行管理、版本化和评估,不包含在此仓库中。该 pipeline 提供了**通用的基线 prompt** (`prompts/baseline/`),它们运行相同的代码路径——这种分离是故意的,本身就是预期的工程信号。 - **无真实数据,无机密。** `data/` 下的每个测试数据都是合成且虚构的;整个代码树中没有任何凭证、真实端点或客户数据(已通过 `gitleaks` + `trufflehog` 验证)。 - **抓取器以通用形式发布——但*展示了其有效性*。** 在生产环境中,该引擎针对真实的、自适应的反爬防御运行;**其目标和目的是专有的并已隐去**,并且本仓库包含**没有任何**特定站点的抓取器。可复用的引擎本身就在这里,`make demo-resilience` 针对**中立的、自托管的**恶劣端点证明了它的能力——展示的是可靠性工程,而不是针对特定站点的规避工具。 - **诚实的数字。** 确定性的保证可以离线复现;生产环境的模型准确率数据被明确标注为生产环境的测量结果,mock 模型并没有被操纵来伪造这些数据。 ## 路线图 / 限制 - [x] 可复现的、离线的、无需密钥的安全评估 (`make eval`) - [x] 可运行的内容图演示 + 一个**反爬韧性 PoC** (`make demo-resilience`) —— 离线、确定性 - [x] 交互式托管仪表板 (Streamlit, mock 模式) —— 安全试验场 + 实时演示 + 评估 - [x] 可运行的、脱敏后的可观测性技术栈 (`infra/observability/`) - [ ] `MODEL_BACKEND=ollama` 演示,复现实时的模型指标 **已知限制(实事求是):** 这是一个*可运行的切片*,而不是整个平台。**视觉阶段**(Qwen-VL 照片分析 + 展示图片选择 + 图像分类)和 CMS/发布集成需要生产环境的图像存储,因此它们**在架构中有所描述,但未作为可运行组件提供**——同样,大多数跨服务的 Temporal 连接也是这种情况(内容 pipeline 在此处独立运行,但外联引擎的真实 Temporal workflow *已包含在内*)。离线演示使用 mock 模型,因此它证明的是*架构和确定性的安全属性*,而不是模型质量。完整的生产系统(持久编排、分布式 SQL、可观测性三件套、CD)在 [METHODS.md](METHODS.md) 和 ADR 中进行了描述,而不是作为代码提供。 ## 联系方式 **Ivans Novikovs** — [github.com/baranlanka](https://github.com/baranlanka) · ivan@novikov.lv ## 许可证 基于 MIT 许可证分发。详见 [`LICENSE`](LICENSE)。
标签:AI平台, Kubernetes, LangGraph, LLM安全防护, Temporal, 多模型, 搜索引擎查询, 用户代理, 自动化外联, 请求拦截, 逆向工具, 酒店旅游