攻略生成模块
攻略生成模块
这个模块实现了一个旅行行程规划服务,主要流程如下:
核心思路是通过前端传来的城市和出游天数信息,先在本地数据库中查找是否已存有相应的旅游数据(例如景点、美食等),如果没有就自动检索和生成对应的旅游信息并存储起来。随后,后端会使用 CAME 库与 Qwen2.5-72B-Instruct 模型,结合一段包含行程规划规则的系统消息,指导大模型生成完整的多日行程安排。为了让结果在前端方便查看,代码会将模型给出的文本格式化成 HTML,并在必要时将其中的图片链接转换成 <img>
标签,使用户可以直接预览行程攻略页面。如果用户想要下载,后端还可以将该 HTML 转换成 PDF 供导出。当用户对当前结果不满意时,可以再次与大模型交互,通过多轮对话来动态调整和优化最终的旅行方案。整个过程如图所示:当有城市与天数信息输入时,系统先判断本地库中是否存在可用数据;若存在则直接调用大模型生成行程,若不存在则先行检索并存储数据后再进行行程生成;最后通过前端界面查看生成的 HTML 或者 PDF,如果用户仍需修改,则再次通过大模型进行迭代。这样就完成了一套从检索数据到生成和展示定制化旅游攻略的完整流程。
import os
import json
import re
from flask import Flask, request, jsonifyfrom camel.configs import QwenConfig
from camel.models import ModelFactory
from camel.types import ModelPlatformType
from camel.toolkits import SearchToolkit
from camel.agents import ChatAgent
from dotenv import load_dotenvload_dotenv()app = Flask(__name__)# 环境变量
os.environ["GOOGLE_API_KEY"] = os.getenv("GOOGLE_API_KEY")
os.environ["SEARCH_ENGINE_ID"] = os.getenv("SEARCH_ENGINE_ID")# 模型初始化
qwen_model = ModelFactory.create(model_platform=ModelPlatformType.OPENAI_COMPATIBLE_MODEL,model_type="Qwen/Qwen2.5-72B-Instruct",api_key=os.getenv("QWEN_API_KEY"),url="https://api-inference.modelscope.cn/v1",model_config_dict=QwenConfig(temperature=0.2).as_dict(),
)tools_list = [*SearchToolkit().get_tools(),
]sys_msg = """
你是一位专业的旅游规划师。请你根据用户输入的旅行需求,包括旅行天数、景点/美食的距离、描述、图片URL、预计游玩/就餐时长等信息,为用户提供一个详细的行程规划。请遵循以下要求:
1. 按照 Day1、Day2、... 的形式组织输出,直到满足用户指定的天数。
2. 每一天的行程请从早餐开始,食物尽量选用当地特色小吃美食,列出上午活动、午餐、下午活动、晚餐、夜间活动(若有),并在末尾总结住宿或返程安排。
3. 对每个景点或美食,提供其基本信息: - 名称- 描述- 预计游玩/就餐时长(如果用户未提供,可以不写或自行估计)- 图片URL(如果有)
4. 请调用在线搜索工具在行程中对移动或出行所需时长做出合理估计。
5. 输出语言为中文。
6. 保持回复简洁、有条理,但必须包含用户想要的所有信息。
"""agent = ChatAgent(system_message=sys_msg,model=qwen_model,message_window_size=10,output_language='Chinese',tools=tools_list
)def create_usr_msg(data: dict) -> str:"""同你原先的实现,用于生成给大模型的用户输入消息"""city = data.get("city", "")days_str = data.get("days", "1")try:days = int(days_str)except ValueError:days = 1lines = []lines.append(f"我准备去{city}旅行,共 {days} 天。下面是我提供的旅行信息:\n")scenic_spots = data.get("景点", [])foods = data.get("美食", [])if scenic_spots:lines.append("- 景点:")for i, spot in enumerate(scenic_spots, 1):lines.append(f" {i}. {spot.get('name', '未知景点名称')}")if '距离' in spot:lines.append(f" - 距离:{spot['距离']}")if 'describe' in spot:lines.append(f" - 描述:{spot['describe']}")if '图片url' in spot:lines.append(f" - 图片URL:{spot['图片url']}")if foods:lines.append("\n- 美食:")for i, food in enumerate(foods, 1):lines.append(f" {i}. {food.get('name', '未知美食名称')}")if 'describe' in food:lines.append(f" - 描述:{food['describe']}")if '图片url' in food:lines.append(f" - 图片URL:{food['图片url']}")lines.append(f"""\n请你根据以上信息,规划一个 {days} 天的行程表。从每天的早餐开始,到晚餐结束,列出一天的行程,包括对出行方式或移动距离的简单说明。如果有多种景点组合,你可以给出最优的路线推荐。请按以下格式输出:Day1:- 早餐:- 上午:- 午餐:- 下午:- 晚餐:...Day2:...Day{days}:...""")return "\n".join(lines)def fix_exclamation_link(text: str) -> str:"""先把类似  的写法,提取出其中的 http://xx.jpg,替换成纯 http://xx.jpg"""md_pattern = re.compile(r'!\[.*?\]\((https?://\S+)\)')# 将  只保留 http://xxx# 比如把 "" -> "http://xx.jpg"return md_pattern.sub(lambda m: m.group(1), text)def convert_picurl_to_img_tag(text: str, width: int = 300, height: int = 200) -> str:"""将文本中的图片URL替换为带样式的HTML img标签,并让图片居中显示和统一大小兼容两步:先处理 ,再匹配 - 图片URL: http://url"""# 第一步:把  变成纯 urltext_fixed = fix_exclamation_link(text)pattern = re.compile(r'-\s*图片URL:\s*(https?://\S+)')replaced_text = pattern.sub(rf'''<div style="text-align: center;"><img src="\1" alt="图片" style="width: {width}px; height: {height}px;" /></div>''',text_fixed)return replaced_textdef generate_cards_html(data_dict):"""生成景点和美食卡片的 HTML 片段"""spots = data_dict.get("景点", [])foods = data_dict.get("美食", [])html_parts = []# 景点推荐html_parts.append("<h2>景点推荐</h2>")if spots:html_parts.append('<div class="card-container">')for spot in spots:name = spot.get("name", "")desc = spot.get("describe", "")distance = spot.get("距离", "")url = spot.get("图片url", "")card_html = f"""<div class="card"><div class="card-image"><img src="{url}" alt="{name}" /></div><div class="card-content"><h3>{name}</h3><p><strong>距离:</strong> {distance}</p><p>{desc}</p></div></div>"""html_parts.append(card_html)html_parts.append("</div>")else:html_parts.append("<p>暂无景点推荐</p>")# 美食推荐html_parts.append("<h2>美食推荐</h2>")if foods:html_parts.append('<div class="card-container">')for food in foods:name = food.get("name", "")desc = food.get("describe", "")url = food.get("图片url", "")card_html = f"""<div class="card"><div class="card-image"><img src="{url}" alt="{name}" /></div><div class="card-content"><h3>{name}</h3><p>{desc}</p></div></div>"""html_parts.append(card_html)html_parts.append("</div>")else:html_parts.append("<p>暂无美食推荐</p>")return "\n".join(html_parts)def generate_html_report(itinerary_text, data_dict):"""将多日行程文本 + 景点美食卡片,合并生成完整HTML"""html_parts = []html_parts.append("<!DOCTYPE html>")html_parts.append("<html><head><meta charset='utf-8'><title>旅行推荐</title>")# 可以内联一些 CSS 样式html_parts.append("<style>")html_parts.append("""body {font-family: "Microsoft YaHei", sans-serif;margin: 20px;background-color: #f8f8f8;line-height: 1.6;}h1, h2 {color: #333;}.itinerary-text {background-color: #fff;padding: 20px;border-radius: 8px;box-shadow: 0 2px 5px rgba(0,0,0,0.1);margin-bottom: 30px;}.card-container {display: flex;flex-wrap: wrap;gap: 20px;margin: 20px 0;}.card {flex: 0 0 calc(300px);border: 1px solid #ccc;border-radius: 10px;overflow: hidden;box-shadow: 0 2px 5px rgba(0,0,0,0.1);background-color: #fff;}.card-image {width: 100%;height: 200px;overflow: hidden;background: #f8f8f8;text-align: center;}.card-image img {max-width: 100%;max-height: 100%;object-fit: cover;}.card-content {padding: 10px 15px;}.card-content h3 {margin-top: 0;margin-bottom: 10px;font-size: 18px;}.card-content p {margin: 5px 0;}.image-center {text-align: center;margin: 20px 0;}.image-center img {width: 300px;height: 200px;object-fit: cover;}""")html_parts.append("</style></head><body>")# 标题html_parts.append("<h1>旅行行程与推荐</h1>")# 行程文本html_parts.append('<div class="itinerary-text">')for line in itinerary_text.split("\n"):if not line.strip():continueif line.strip().startswith("Day"):html_parts.append(f"<h2>{line.strip()}</h2>")else:html_parts.append(f"<p>{line}</p>")html_parts.append('</div>')# 景点/美食卡片cards_html = generate_cards_html(data_dict)html_parts.append(cards_html)html_parts.append("</body></html>")return "\n".join(html_parts)def save_html_file(city: str, days: str, html_content: str) -> str:"""保存HTML内容到文件Args:city: 城市名days: 旅行天数html_content: HTML内容Returns:str: 保存的文件路径"""# 确保storage目录存在storage_dir = "storage"if not os.path.exists(storage_dir):os.makedirs(storage_dir)# 生成文件名filename = f"{storage_dir}/{city}{days}天旅游攻略.html"# 保存HTML内容with open(filename, "w", encoding="utf-8") as f:f.write(html_content)return filename@app.route("/generate_itinerary_html", methods=["POST"])
def generate_itinerary_html():"""请求 JSON 格式:{"city": "成都","days": "3"}返回生成的HTML文件路径和内容"""req_data = request.json or {}city = req_data.get("city", "")days = req_data.get("days", "1")json_filename = f"storage/{city}{days}天旅游信息.json"if not os.path.exists(json_filename):return jsonify({"error": f"文件 {json_filename} 不存在,请检查输入的目的地和天数!"}), 404try:with open(json_filename, "r", encoding="utf-8") as f:data = json.load(f)except json.JSONDecodeError:return jsonify({"error": f"文件 {json_filename} 格式错误,请检查文件内容!"}), 400# 1. 生成用户输入并调用大模型usr_msg = create_usr_msg(data)response = agent.step(usr_msg)# 2. 将模型输出中的图片URL替换成 <img ... />model_output = response.msgs[0].contentend_output = convert_picurl_to_img_tag(model_output)# 3. 生成完整 HTML 报告html_content = generate_html_report(end_output, data)# 4. 保存HTML文件saved_file = save_html_file(city, days, html_content)# 5. 返回文件路径和HTML内容return jsonify({"file_path": saved_file,"html_content": html_content}), 200if __name__ == "__main__":app.run(host="0.0.0.0", port=5003, debug=True)
这里我们使用另一种方式来测试服务接口,熟悉软件开发的小伙伴应该用过Postman
我们在postman中使用Post请求发送一份数据来模拟前端的请求,从而得到后端返回的数据,即html_content,然后由save逻辑保存到本地的storage数据库中
此时其实我们已经得到了一份不错的可展示的攻略了,在本地的浏览器中双击即可打开并看到渲染效果
成都3天旅游攻略.html
最后我们再把HTML转成在任何显示设备上都一致的PDF格式,保证了攻略的统一性,不必担心错位,换行等一系列问题。
相关文章:
攻略生成模块
攻略生成模块 这个模块实现了一个旅行行程规划服务,主要流程如下: 核心思路是通过前端传来的城市和出游天数信息,先在本地数据库中查找是否已存有相应的旅游数据(例如景点、美食等),如果没有就自动检索和…...

海康NVR录像回放SDK原始流转FLV视频流:基于Java的流媒体转码(无需安装第三方插件ffmpeg)
wlinker-video-monitor 代码地址:https://gitee.com/wlinker/wlinker-video-monitor 背景与需求 在安防监控、智能楼宇等场景中,海康威视设备作为行业主流硬件,常需要将录像回放功能集成到Web系统中。然而,海康设备的原始视频流…...
深入理解设计模式:工厂模式、单例模式
深入理解设计模式:工厂模式、单例模式 设计模式是软件开发中解决常见问题的可复用方案。本文将详细介绍两种种重要的创建型设计模式:工厂模式、单例模式,并提供Java实现示例。 一、工厂模式 工厂模式是一种创建对象的设计模式,…...

运维Linux之Ansible详解学习(更新中)
什么是Ansible Ansible 是一款新出现的自动化运维工具,基于 Python 开发。以下是对它的详细介绍: 功能特点:集合了众多运维工具的优点,能实现批量系统配置、批量程序部署、批量运行命令等功能。它是基于模块工作的,本…...

深入浅出IIC协议 - 从总线原理到FPGA实战开发 -- 第三篇:Verilog实现I2C Master核
第三篇:Verilog实现I2C Master核 副标题 :从零构建工业级I2C控制器——代码逐行解析与仿真实战 1. 架构设计 1.1 模块分层设计 三层架构 : 层级功能描述关键信号PHY层物理信号驱动与采样sda_oe, scl_oe控制层协议状态机与数据流控制state…...
网络世界的“变色龙“:动态IP如何重构你的数据旅程?
在深秋的下午调试代码时,我偶然发现服务器日志中出现异常登录记录——IP地址显示为某个境外数据中心。更有趣的是,当我切换到公司VPN后,这个"可疑IP"竟自动消失在了防火墙监控列表中。这个瞬间让我意识到:现代网络架构中…...
进阶-自定义类型(结构体、位段、枚举、联合)
自定义类型:结构体,枚举,联合 结构体 结构体类型的声明 结构的自引用 结构体变量的定义和初始化 结构体内存对齐 结构体传参 结构体实现位段(位段的填充&可移植性) 枚举 枚举类型的定义 枚举的优点 枚举的使用 联合 联合类型的定义 联…...
5G 网络全场景注册方式深度解析:从信令交互到报文分析
摘要 本文全面梳理 5G 网络包含的初始注册、移动性注册更新、紧急注册、周期性注册更新、服务请求触发注册、切换触发注册、基于策略的注册更新等多种注册方式。详细阐述每种注册方式的触发条件、信令流程、关键报文结构,结合对比分析与实际案例,助力读者深入理解 5G 网络接…...

ARM笔记-嵌入式系统基础
第一章 嵌入式系统基础 1.1嵌入式系统简介 1.1.1嵌入式系统定义 嵌入式系统定义: 嵌入式系统是以应用为中心,以计算机技术为基础,软硬件可剪裁,对功能、可靠性、成本、体积、功耗等有严格要求的专用计算机系统 ------Any devic…...
一文讲透golang channel 的特点、原理及使用场景
在 Go 语言中,通道(Channel) 是实现并发编程的核心机制之一,基于 CSP(Communicating Sequential Processes) 模型设计。它不仅用于协程(Goroutine)之间的数据传递,还通过…...

upload-labs通关笔记-第19关文件上传之条件竞争
系列目录 upload-labs通关笔记-第1关 文件上传之前端绕过(3种渗透方法) upload-labs通关笔记-第2关 文件上传之MIME绕过-CSDN博客 upload-labs通关笔记-第3关 文件上传之黑名单绕过-CSDN博客 upload-labs通关笔记-第4关 文件上传之.htacess绕过-CSDN…...

第5章:任务间通信机制(IPC)全解析
💬 在多线程开发中,线程之间如何协作?如何让一个线程产生数据,另一个线程消费数据?本章聚焦 Zephyr 提供的多种任务间通信机制(IPC)及实战使用技巧。 📚 本章导读 你将学到: Zephyr 提供的常用 IPC 接口:FIFO、消息队列、邮箱、信号量 每种机制适用场景和用法对比…...

CAPL自动化-诊断Demo工程
文章目录 前言一、诊断控制面板二、诊断定义三、发送诊断通过类.方法的方式req.SetParameterdiagSetParameter四、SendRequestAndWaitForResponse前言 本文将介绍CANoe的诊断自动化测试,工程可以从CANoe的 Sample Configruration 界面打开,也可以参考下面的路径中打开(以实…...

SVN被锁定解决svn is already locked
今天遇到一个问题,svn 在提交代码的时候出现了svn is already locked,解决方案...

【深度学习】1. 感知器,MLP, 梯度下降,激活函数,反向传播,链式法则
一、感知机 对于分类问题,我们设定一个映射,将x通过函数f(x)映射到y 1. 感知机的基本结构 感知机(Perceptron)是最早期的神经网络模型,由 Rosenblatt 在 1958 年提出,是现代神经网络和深度学习模型的雏形…...

云原生安全:网络协议TCP详解
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 (注:文末附可视化流程图与专有名词说明表) 1. 基础概念 TCP(Transmission Control Protocol)是…...

使用CentOS部署本地DeekSeek
一、查看服务器的操作系统版本 cat /etc/centos-release二、下载并安装ollama 1、ollama下载地址: Releases ollama/ollama GitHubGet up and running with Llama 3.3, DeepSeek-R1, Phi-4, Gemma 3, Mistral Small 3.1 and other large language models. - Re…...
Spring Boot与Eventuate Tram整合:构建可靠的事件驱动型分布式事务
精心整理了最新的面试资料和简历模板,有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 一、引言 在现代微服务架构中,分布式事务管理一直是复杂系统中的核心挑战之一。传统的两阶段提交(2PC)方案存在性能瓶颈&…...
Python:从脚本语言到工业级应用的传奇进化
一、Python的诞生:一场喜剧与编程的奇妙相遇 1989年的冬天,荷兰程序员Guido van Rossum在阿姆斯特丹的CWI研究所里,用一段独特的代码开启了编程语言的新纪元。这个被命名为"Python"的项目,灵感并非源自冷血的蟒蛇,而是源于Guido对英国喜剧团体Monty Python的痴…...
【排序算法】典型排序算法 Java实现
以下是典型的排序算法分类及对应的 Java 实现,包含时间复杂度、稳定性说明和核心代码示例: 一、比较类排序(通过元素比较) 1. 交换排序 ① 冒泡排序 时间复杂度:O(n)(优化后最优O(n)) 稳定性&…...
node.js如何实现双 Token + Cookie 存储 + 无感刷新机制
node.js如何实现双 Token Cookie 存储 无感刷新机制 为什么要实施双token机制? 优点描述安全性Access Token 短期有效,降低泄露风险;Refresh Token 权限受限,仅用于获取新 Token用户体验用户无需频繁重新登录,Toke…...
[DS]使用 Python 库中自带的数据集来实现上述 50 个数据分析和数据可视化程序的示例代码
使用 Python 库中自带的数据集来实现上述 50 个数据分析和数据可视化程序的示例代码 摘要:由于 sample_data.csv 是一个占位符文件,用于代表任意数据集,我将使用 Python 库中自带的数据集来实现上述 50 个数据分析和数据可视化程序的示例代码…...
探索智能仓颉
探索智能仓颉:Cangjie Magic体验有感 一、引言 在人工智能和智能体开发领域,新的技术和框架不断涌现,推动着行业的快速发展。2025年3月,仓颉社区开源了Cangjie Magic,这是一个基于仓颉编程语言原生构建的LLM Agent开…...
Ubuntu 上开启 SSH 服务、禁用密码登录并仅允许密钥认证
1. 安装 OpenSSH 服务 如果尚未安装 SSH 服务,运行以下命令: sudo apt update sudo apt install openssh-server2. 启动 SSH 服务并设置开机自启 sudo systemctl start ssh sudo systemctl enable ssh3. 生成 SSH 密钥对(本地机器…...

LLMs之Qwen:《Qwen3 Technical Report》翻译与解读
LLMs之Qwen:《Qwen3 Technical Report》翻译与解读 导读:Qwen3是Qwen系列最新的大型语言模型,它通过集成思考和非思考模式、引入思考调度机制、扩展多语言支持以及采用强到弱的知识等创新技术,在性能、效率和多语言能力方面都取得…...
springboot3 configuration
1 多数据库配置 github: https://github.com/baomidou/dynamic-datasource 使用DS()注解来切换数据库 详情介绍:https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611 注意:DS 可以注解在方法上或类上,同时存在就近原则 方法上注…...

从工程实践角度分析H.264与H.265的技术差异
作为音视频从业者,我们时刻关注着视频编解码技术的最新发展。RTMP推流、轻量级RTSP服务、RTMP播放、RTSP播放等模块是大牛直播SDK的核心功能,在这些模块的实现过程中,H.264和H.265两种视频编码格式的应用实践差异是我们技术团队不断深入思考的…...

如何设计一个高性能的短链设计
1.什么是短链 短链接(Short URL) 是通过算法将长 URL 压缩成简短字符串的技术方案。例如将 https://flowus.cn/veal/share/3306b991-e1e3-4c92-9105-95abf086ae4e 缩短为 https://sourl.cn/aY95qu,用户点击短链时会自动重定向到原始长链接。其…...

提升工作效率的可视化笔记应用程序
StickyNotes桌面便签软件介绍 StickyNotes是一款极为简洁的桌面便签应用程序,让您能够快速记录想法、待办事项或其他重要信息。这款工具操作极其直观,只需输入文字内容,选择合适的字体大小和颜色,然后点击添加按钮即可创建个性化…...

11|省下钱买显卡,如何利用开源模型节约成本?
不知道课程上到这里,你账户里免费的5美元的额度还剩下多少了?如果你尝试着完成我给的几个数据集里的思考题,相信这个额度应该是不太够用的。而ChatCompletion的接口,又需要传入大量的上下文信息,实际消耗的Token数量其…...