LangChain + Ollama + Tavily 实现旅游问答系统

一、项目简介

最近在学习 LangChain 框架和本地大模型 Ollama,动手写了一个景点 + 天气问答小项目。项目功能简单,主要用来练手 LangChain 常用组件、分支路由、并行调用等基础知识点,全程本地运行,不用额外付费,适合新手跟着敲代码学习。

二、整体业务流程图

执行流程简图

模块分工说明

  1. 意图解析模块:从自然语言中提取地点、查询类型,转为标准 JSON 结构化数据;

  2. 分支路由模块:根据查询类型自动分流,区分是否需要联网查天气;

  3. RAG 检索模块:基于内存向量库,检索预设景点资料;

  4. 联网搜索模块:调用 Tavily 获取实时天气信息,并解析结构化返回结果;

  5. 回答生成模块:汇总多源信息,由大模型整理成通顺的旅游建议。

三、环境准备

安装依赖包

复制代码
pip install langchain langchain-ollama langchain-tavily python-dotenv

本地部署 Ollama 模型

提前安装 Ollama 客户端,拉取项目所需模型:

复制代码
# 对话大模型
ollama pull qwen:7b
# 文本向量化模型
ollama pull dengcao/Qwen3-Embedding-0.6B:Q8_0

配置 Tavily 密钥

  1. 访问 Tavily 官网 注册账号,免费版每月提供 1000 次调用,足够日常使用;

  2. 在项目根目录新建 .env 文件,写入密钥:

    TAVILY_API_KEY=你的Tavily密钥

四、完整项目代码

python 复制代码
import os
from langchain_core.runnables import RunnableLambda, RunnableBranch, RunnableMap
from langchain_core.output_parsers import JsonOutputParser
from langchain_tavily import TavilySearch
from langchain_ollama import OllamaLLM, OllamaEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.exceptions import OutputParserException
from dotenv import load_dotenv

load_dotenv()

# ==================== 基础配置项 ====================
LLM_MODEL = "qwen:7b"
EMBED_MODEL = "dengcao/Qwen3-Embedding-0.6B:Q8_0"
RETRIEVE_NUM = 1
EMPTY_ATTR_TIP = "暂时没有该景点的介绍信息"
NO_WEATHER_TIP = "未查询到当日天气数据"

# 从环境变量中读取Tavily平台的API密钥
os.environ["TAVILY_API_KEY"] = os.getenv("TAVILY_API_KEY")


class TravelQA:
    """
    旅游问答系统主类
    整合意图识别、知识库检索、联网搜索、分支路由、答案生成全流程
    """
    def __init__(self):
        # 初始化本地大模型,负责语义理解、指令解析与内容生成
        self.llm = OllamaLLM(model=LLM_MODEL)
        # 初始化向量模型,将文本转为向量,实现相似度检索
        self.embedding = OllamaEmbeddings(model=EMBED_MODEL)
        # 初始化联网搜索工具,限定最多返回1条搜索结果
        self.search_tool = TavilySearch(max_results=1)

        # 自定义景点知识库,存储北京知名景点的基础游玩信息
        self.attraction_data = [
            "故宫:位于北京中轴线中心,是明清两代皇家宫殿,中国现存规模最大的木质古建筑群。开放时间8:30-17:00,16:00停止入园,周一闭馆(法定节假日除外)。门票60元,建议游玩3小时,主要看点有太和殿、乾清宫、御花园等。",
            "颐和园:北京著名皇家园林,以昆明湖、万寿山为主体,集江南园林之大成。开放时间7:00-18:00,门票30元,联票60元。园内长廊、佛香阁、十七孔桥为核心景点,适合散步休闲、观景拍照,建议游玩2-3小时。",
            "八达岭长城:位于北京延庆区,距离市区约70公里,是明长城最具代表性的段落。开放时间7:30-17:30,门票40元。地势雄伟、视野开阔,建议游玩3-4小时,适合半日出游,体力一般可选择索道上下。",
            "天坛公园:明清帝王祭天、祈谷的场所,是中国现存最大的古代祭祀建筑群。开放时间6:00-22:00,景区8:00开放核心建筑,门票15元联票35元。主要景点祈年殿、圜丘坛、回音壁,环境清幽,适合晨练与观光。",
            "圆明园:清代大型皇家园林,有万园之园之称,遗址公园保留大量历史遗迹。开放时间7:00-19:00,门票25元,联票25元,遗址区适合历史观赏、散步游览,建议游玩2小时,园内湖景绿化优美。"
        ]

        # 初始化内存向量库,把景点文本批量向量化并存储
        self.vector_db = InMemoryVectorStore.from_texts(
            texts=self.attraction_data,
            embedding=self.embedding
        )
        # 用于存储组装完成的完整执行链路
        self.full_chain = None

    def build_intent_chain(self):
        """
        构建意图解析链路
        作用:从用户自然语言提问中,提取地点、查询类型,输出标准JSON结构化数据
        """
        prompt = ChatPromptTemplate.from_messages([
            ("system", "你是问答助手,从用户问题里提取地点和查询类型,类型分为:查询天气、景点介绍、行程规划"),
            ("user", "问题:{user_question},请只返回json格式:{{'location':'地点','type':'查询类型'}}")
        ])
        # 链路组合:提示词模板 -> 大模型 -> JSON解析器
        return prompt | self.llm | JsonOutputParser()

    def build_attr_retrieve_chain(self):
        """
        构建景点信息检索链路
        作用:根据解析出的地点,在向量库中匹配对应的景点介绍
        """
        return (
            # 提取入参中的地点字段
            RunnableLambda(lambda x: x["location"])
            # 调用向量库执行相似度检索
            | self.vector_db.as_retriever(search_kwargs={"k": RETRIEVE_NUM})
            # 处理检索结果,无匹配内容则返回兜底文案
            | RunnableLambda(lambda res: res[0].page_content if res else EMPTY_ATTR_TIP)
        )

    def build_parallel_chain(self):
        """
        构建并行执行链路
        作用:同时执行景点检索和天气联网搜索,提升整体响应效率
        """
        # 封装天气查询逻辑,处理搜索结果解析
        def get_weather_info(data):
            location = data["location"]
            # 定制搜索关键词,提升天气信息检索准确率
            query = f"{location} 今日实时天气 气温 风力"
            search_res = self.search_tool.invoke(query)
            # 解析Tavily返回的结构化数据,提取纯文本内容
            if isinstance(search_res, list) and len(search_res) > 0:
                return search_res[0].get("content", NO_WEATHER_TIP)
            return NO_WEATHER_TIP

        # 天气查询子链路
        weather_chain = RunnableLambda(get_weather_info)
        # 景点检索子链路
        attr_chain = self.build_attr_retrieve_chain()

        # RunnableMap 实现多任务并行,汇总多路结果
        return RunnableMap({
            "location": RunnableLambda(lambda x: x["location"]),
            "weather": weather_chain,
            "attraction": attr_chain
        })

    def build_branch_chain(self):
        """
        构建分支路由链路
        作用:根据查询类型自动分流,区分是否需要联网查询天气
        """
        parallel_chain = self.build_parallel_chain()
        attr_chain = self.build_attr_retrieve_chain()

        return RunnableBranch(
            # 分支1:查询类型包含天气,执行并行查询链路
            (lambda x: "天气" in x["type"], parallel_chain),
            # 分支2:仅查询景点,只读取本地知识库,不发起联网请求
            lambda x: {
                "location": x["location"],
                "attraction": attr_chain.invoke(x),
                "weather": NO_WEATHER_TIP
            }
        )

    def build_generate_chain(self):
        """
        构建答案生成链路
        作用:整合景点资料、天气数据,由大模型整理成通顺的回复内容
        """
        prompt = ChatPromptTemplate.from_messages([
            ("system", "结合地点介绍和天气情况,整理简洁实用的旅游回答,包含游玩建议和出行注意事项。"),
            ("user", "地点:{location}\n景点信息:{attraction}\n天气情况:{weather}")
        ])
        # 生成回答并去除文本首尾多余空格
        return prompt | self.llm | RunnableLambda(lambda text: text.strip())

    def create_full_chain(self):
        """
        组装完整执行链路
        按流程串联意图解析、分支路由、信息检索、答案生成所有模块
        """
        self.full_chain = (
            self.build_intent_chain()
            # 精简传递参数,只保留下游需要的字段
            | RunnableLambda(lambda res: {"location": res["location"], "type": res["type"]})
            | self.build_branch_chain()
            | self.build_generate_chain()
        )

    def ask_question(self, question):
        """
        对外统一调用入口
        接收用户提问,调用完整链路并返回最终回答
        """
        # 校验链路是否完成初始化
        if not self.full_chain:
            return "请先初始化执行链路"
        try:
            return self.full_chain.invoke({"user_question": question})
        # 捕获JSON解析异常
        except OutputParserException:
            return "解析内容失败,请重新提问"
        # 捕获其余运行时异常
        except Exception as e:
            return f"运行出错:{str(e)}"


if __name__ == "__main__":
    # 实例化问答系统对象
    qa = TravelQA()
    # 加载并组装完整执行链路
    qa.create_full_chain()

    # 测试用例1:同时查询景点与天气
    print("===== 测试1 =====")
    q1 = "故宫今天天气怎么样?适合游玩吗?"
    print(f"提问:{q1}")
    print(f"回答:{qa.ask_question(q1)}\n")

    # 测试用例2:纯景点介绍查询
    print("===== 测试2 =====")
    q2 = "介绍一下八达岭长城"
    print(f"提问:{q2}")
    print(f"回答:{qa.ask_question(q2)}\n")

    # 测试用例3:景点游玩攻略查询
    print("===== 测试3 =====")
    q3 = "天坛公园游玩攻略"
    print(f"提问:{q3}")
    print(f"回答:{qa.ask_question(q3)}")

五、运行效果演示

python 复制代码
===== 测试1 =====
提问:故宫今天天气怎么样?适合游玩吗?
回答:根据提供的信息,您计划在故宫参观。以下是一些建议和注意事项:

**游玩建议:**
1. 提前购票:为了避免现场排队,建议网上预订门票。
2. 选择合适的游览时间:由于16:00停止入园,尽量避免在此时之后抵达。
3. 利用导览服务或自行规划路线,重点关注太和殿、乾清宫、御花园等主要景点。

**出行注意事项:**
1. 保持个人卫生,尤其是在触摸公共设施后务必洗手。
2. 穿着舒适且适合天气的衣物,北京夏季可能较为炎热。
3. 随身携带必要的物品,如相机、水瓶和轻便食物(如有需要)。
4. 注意保管好自己的贵重物品,避免在人群中丢失。

===== 测试2 =====
提问:介绍一下八达岭长城
回答:考虑到您提供的是八达岭长城景区的信息,以下是针对您的旅游回答:

**目的地与景点信息:**
八达岭长城位于北京市延庆区,是明长城最具标志性的部分。门票价格为40元,开放时间为7:30-17:30。

**游玩建议:**
建议您安排3-4小时的游览时间,既可体验长城的雄伟壮观,又不会过于劳累。如果体力一般,可以选择索道上下,节省体力。

**出行注意事项:**
由于没有实时查询到当日天气数据,请您在出发前再次确认当天天气情况,以便携带适合的衣物和防晒用品。

===== 测试3 =====
提问:天坛公园游玩攻略
回答:旅游建议:

1. 早上游玩:天坛公园开放时间早,你可以利用这段时间在园内进行晨练或轻松观光。

2. 主要景点推荐:祈年殿、圜丘坛和回音壁都是不可错过的经典建筑。可以围绕这些地点规划游览路线。

3. 注意事项:天气请随时关注,避免因突变的天气而影响行程。此外,保护文物环境,不在禁止拍照的区域拍摄。

祝你在天坛公园度过愉快的一天!

启动代码后会依次执行三组测试用例:

  1. 查询故宫天气:正常检索景点资料 + 拉取实时天气,结合两者生成游玩建议;

  2. 介绍八达岭长城:仅读取本地知识库内容,无联网请求;

  3. 天坛公园游玩攻略:识别为景点查询,输出景点基础信息与游玩提示。

六、后续拓展方向

  1. 替换 InMemoryVectorStore 为 Chroma/FAISS,实现向量数据持久化;

  2. 读取本地 PDF/Markdown 文件,批量导入景点知识库;

  3. 增加多轮对话记忆,实现上下文连续问答;

  4. 新增「行程规划」分支,丰富业务场景。

八、总结

本项目整合了 LangChain 多个核心组件,完整覆盖意图识别、RAG 检索、并行任务、条件路由、工具调用等常用能力。搭配 Ollama 本地模型 + Tavily 免费搜索,零成本即可跑通整套流程。

相关推荐
Solis程序员1 小时前
LangChain从入门到精通(1)
langchain
追梦人电立电子1 小时前
X、Y电容的分类与选择
人工智能·分类·数据挖掘·追梦人电力电子
美狐美颜SDK开放平台1 小时前
直播APP开发实战:第三方美颜sdk接入步骤与注意事项
人工智能·音视频·美颜sdk·第三方美颜sdk·短视频美颜sdk
yychen_java1 小时前
当算法成为武器:AI泛滥时代的多维危机透视与治理路径
网络·人工智能·ai
伊布拉西莫1 小时前
【流畅的Python】第20章:并发执行器 — 学习笔记
笔记·python·学习
TomatoStudy1 小时前
IT职业教育AI落地与实训体系建设复盘——以职坐标模式为例
大数据·人工智能
大模型最新论文速读1 小时前
小红书提出 RedKnot:分头处理 kv 缓存,延时降低 60%效果还提升
论文阅读·人工智能·深度学习·机器学习·缓存·自然语言处理
阿瑞IT1 小时前
AI Agent 工具调用可靠性的工程实践
人工智能
IT策士1 小时前
Redis 从入门到精通:Python 操作 Redis
redis·python·bootstrap