一、项目简介
最近在学习 LangChain 框架和本地大模型 Ollama,动手写了一个景点 + 天气问答小项目。项目功能简单,主要用来练手 LangChain 常用组件、分支路由、并行调用等基础知识点,全程本地运行,不用额外付费,适合新手跟着敲代码学习。
二、整体业务流程图
执行流程简图
模块分工说明
-
意图解析模块:从自然语言中提取地点、查询类型,转为标准 JSON 结构化数据;
-
分支路由模块:根据查询类型自动分流,区分是否需要联网查天气;
-
RAG 检索模块:基于内存向量库,检索预设景点资料;
-
联网搜索模块:调用 Tavily 获取实时天气信息,并解析结构化返回结果;
-
回答生成模块:汇总多源信息,由大模型整理成通顺的旅游建议。
三、环境准备
安装依赖包
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 密钥
-
访问 Tavily 官网 注册账号,免费版每月提供 1000 次调用,足够日常使用;
-
在项目根目录新建
.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. 注意事项:天气请随时关注,避免因突变的天气而影响行程。此外,保护文物环境,不在禁止拍照的区域拍摄。
祝你在天坛公园度过愉快的一天!
启动代码后会依次执行三组测试用例:
-
查询故宫天气:正常检索景点资料 + 拉取实时天气,结合两者生成游玩建议;
-
介绍八达岭长城:仅读取本地知识库内容,无联网请求;
-
天坛公园游玩攻略:识别为景点查询,输出景点基础信息与游玩提示。
六、后续拓展方向
-
替换
InMemoryVectorStore为 Chroma/FAISS,实现向量数据持久化; -
读取本地 PDF/Markdown 文件,批量导入景点知识库;
-
增加多轮对话记忆,实现上下文连续问答;
-
新增「行程规划」分支,丰富业务场景。
八、总结
本项目整合了 LangChain 多个核心组件,完整覆盖意图识别、RAG 检索、并行任务、条件路由、工具调用等常用能力。搭配 Ollama 本地模型 + Tavily 免费搜索,零成本即可跑通整套流程。