用 LangChain + 通义千问,打造一个能查天气、能搜新闻的多任务问答助手
实战:用 LangChain + 通义千问,打造一个能查天气、能搜新闻的多任务问答助手
前段时间拿到一个基于 LangChain 构建的多任务问答助手项目,本来跑的是 OpenAI 的 gpt-4o,在国内网络环境下体验不佳。索性花了一个晚上把它整体迁移到国产大模型——阿里云通义千问(qwen-max),把架构、代码、踩坑记录都整理了出来。这篇文章不讲大道理,全部是能跑通的代码和真实运行日志,希望对想入门 LangChain 工具调用(Function Calling)的同学有帮助。
为什么写这篇?
先看三个现状:
- LangChain 的教程多,但"完整可运行的项目"少——多数教程只讲一个
llm.invoke()就结束了,工具调用、日志、配置管理这些工程细节全靠自己摸索。 - OpenAI 的项目在国内迁移有真实需求——API 访问不稳定、key 获取门槛高,而国产模型(通义千问)已经提供了 OpenAI 兼容接口,切换成本极低。
- "能查天气 + 能搜新闻"的 Agent 是绝佳的入门案例——麻雀虽小,五脏俱全:配置层、日志层、工具层、代理层全都有。
这篇文章将带你完整拆解这个项目,并给出国产化迁移的完整方案。
一、这个助手能做什么?
项目是一个 CLI 交互式问答助手,支持三类能力:
| 能力 | 示例提问 | 背后调用的工具 |
|---|---|---|
| 💬 日常对话 | “你好,你能做什么?” | 无(LLM 直接回答) |
| 🌤️ 天气查询 | “查询北京今天的天气” | 高德地图天气 API |
| 🔍 信息搜索 | “搜索最新的人工智能新闻” | Tavily 搜索 API |
核心机制是 Function Calling(函数调用):LLM 不再是"只会说话的聊天机器人",它能根据用户问题自己决定是否需要调用工具、调用哪个工具、传什么参数。
用户提问 "查询北京天气"
↓
qwen-max 分析:这个问题需要工具 → 决定调用 weather_query(city_name="北京")
↓
程序执行高德 API,拿到天气数据
↓
LLM 把结构化数据组织成自然语言回答:"北京今天 32°C,晴..."
二、整体架构:教科书式的分层
项目虽然不大,但分层非常清晰,是一个标准的分层架构(Layered Architecture):
┌───────────────────────────────────────────────┐
│ 入口层 main.py(CLI 交互循环) │
├───────────────────────────────────────────────┤
│ 代理层 agents/qa_agent.py(QAAgent) │
├───────────────────────────────────────────────┤
│ 工具层 tools/(高德天气 + Tavily 搜索) │
├───────────────────────────────────────────────┤
│ 配置层 config/settings.py(Pydantic) │
│ 基础设施 core/logger.py(loguru 日志) │
└───────────────────────────────────────────────┘
依赖方向是单向的:tools → agents → main,配置层和日志层作为基础设施被各层共享,没有循环依赖。这在中小型项目中是非常正确的选择——不要一上来就上微服务,分层架构足够应对 90% 的场景。
📌 工程经验:分层架构的核心价值是"依赖单向"。如果哪天你发现
tools里的代码反过来 import 了agents,说明分层已经坏了,要尽早拆。
三、环境准备:一套依赖 + 一个 .env
3.1 依赖安装
项目依赖集中在 requirements.txt,核心只有几个:
pip install langchain langchain-openai langchain-core \
tavily-python python-dotenv pydantic pydantic-settings \
loguru -i https://pypi.tuna.tsinghua.edu.cn/simple
如果已有 conda 环境,建议在独立环境(如
llmops)中安装,避免污染全局 Python。
3.2 环境变量:一套 key 走天下
项目所有密钥统一放在 .env 中,由 python-dotenv 加载。这里有个关键点——迁移到国产模型后,我们让 DashScope 的 key "伪装"成 OpenAI 的 key:
# OpenAI 兼容 API 配置(实际指向阿里云 DashScope)
OPENAI_API_KEY=sk-你的DashScope密钥
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
# LLM 模型名(通义千问)
LLM_MODEL=qwen-max
# 高德地图(天气查询)
AMAP_API_KEY=你的高德Web服务key
# Tavily 搜索
TAVILY_API_KEY=你的Tavily key
为什么能这样?因为通义千问提供了 OpenAI 兼容模式——接口路径、请求格式、返回结构都和 OpenAI 一致。所以 LangChain 的 ChatOpenAI 类不需要换,只需要换 base_url 和模型名。
四、核心模块逐层拆解
4.1 配置层:Pydantic + 单例模式
配置模块用 Pydantic 做数据校验,这是企业级项目的标准做法:
class APISettings(BaseSettings):
openai_api_key: str = Field(..., description="API密钥")
amap_api_key: str = Field(..., description="高德API密钥")
@validator('openai_api_key', 'amap_api_key')
def validate_api_keys(cls, v):
"""验证API密钥不能为空"""
if not v or v.strip() == "":
raise ValueError("API密钥不能为空")
return v.strip()
class Settings:
"""全局配置管理器 - 单例模式"""
_instance = None
_initialized = False
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
两个亮点:
- Pydantic 校验:key 为空、端口越界、日志级别非法,都会在启动时直接报错,而不是运行到一半才炸
- 单例模式:整个程序只有一份配置实例,避免多处读取导致的不一致
4.2 日志层:loguru 四路输出
日志模块是项目里最"专业"的部分,用 loguru 配置了四路输出:
| 输出目标 | 记录内容 | 保留策略 |
|---|---|---|
| 控制台 | 全部日志(带颜色) | — |
app_日期.log |
应用全量日志 | 30 天,按天轮转 |
error_日期.log |
仅 ERROR 级别 | 90 天 |
api_日期.log |
API 调用专项日志 | 7 天,zip 压缩 |
logger.add(
sys.stdout,
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
"<level>{level: <8}</level> | {message}",
level="INFO", colorize=True
)
logger.add(
os.path.join(log_dir, "error_{time:YYYY-MM-DD}.log"),
level="ERROR", rotation="00:00", retention="90 days", compression="zip"
)
📌 工程经验:日志不是"print 的升级版",而是排障的第一现场。API 调用日志单独落文件、错误日志长保留、按天轮转压缩——这三条够用 90% 的场景。
4.3 工具层:统一返回结构
每个外部 API 封装成一个类,所有工具返回统一的 {success, data, error} 结构:
class AmapWeatherTool:
def get_weather(self, city_name: str) -> Dict[str, Any]:
try:
...
return {"success": True, "data": formatted_data}
except requests.exceptions.Timeout:
return {"success": False, "error": "请求超时,请稍后重试"}
except Exception as e:
return {"success": False, "error": f"获取天气信息失败: {str(e)}"}
同时用 Pydantic 定义工具的参数 Schema,让 LLM 知道该传什么参数:
class WeatherQuery(BaseModel):
"""天气查询工具"""
city_name: str = Field(..., description="要查询天气的城市名称,例如:北京、上海、广州等")
📌 工程经验:统一的返回结构让上层代码永远不需要判断"这个工具返回了什么形状",只需检查 success。这是工具层设计的黄金法则。
4.4 代理层:LLM 自己决定要不要调工具
这是整个项目最核心的机制——bind_tools:
self.llm = ChatOpenAI(
model=os.getenv("LLM_MODEL", "qwen-plus"), # qwen-max
api_key=settings.api.openai_api_key,
base_url=settings.api.openai_base_url, # DashScope 兼容端点
temperature=0.3,
max_tokens=1000
)
self.llm_with_tools = self.llm.bind_tools(self.tools)
bind_tools 把工具列表"绑定"到 LLM 上,之后 LLM 的返回值中会包含 tool_calls 字段——LLM 自己决定要不要调用工具:
response = self.llm_with_tools.invoke(user_input)
if response.tool_calls: # LLM 认为需要调用工具
for tool_call in response.tool_calls:
tool_name = tool_call['name']
tool_args = tool_call['args']
# 执行对应工具 ...
else: # 普通对话,直接回答
final_response = self.general_chain.invoke({"query": user_input})
用一张图理解整个调用流程:
五、国产化迁移:从 gpt-4o 到 qwen-max
迁移过程其实只有 三步,全程半小时:
第 1 步:改模型名
DashScope 的 OpenAI 兼容层不支持 gpt-4o 等 OpenAI 模型名,必须换成通义千问的模型标识:
# 修改前
model="gpt-4o"
# 修改后(从环境变量读取,更灵活)
model=os.getenv("LLM_MODEL", "qwen-plus") # .env 中设为 qwen-max
第 2 步:改 base_url
# 修改前
OPENAI_BASE_URL=https://api.openai.com/v1
# 修改后
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
第 3 步:补依赖 + 跑起来
环境里缺了 tavily-python,安装后直接运行:
pip install tavily-python -i https://pypi.tuna.tsinghua.edu.cn/simple
python main.py
注意:ChatOpenAI 类完全不用换——这就是 OpenAI 兼容模式的最大价值,应用层零改动。
六、运行效果实录
日常对话
您: hello
正在思考...
助手: 你好!有什么可以帮助你的吗?如果想了解天气,可以告诉我"查询XX城市天气";
如果需要搜索信息,可以说"搜索XX"。
⏱️ 处理时间: 1577.2ms
工具调用(自动识别意图 + 自动传参)
您: 查询北京今天的天气
正在思考...
🔧 检测到工具调用: 1个
📞 调用工具: weather_query, 参数: {'city_name': '北京'}
助手: 您好,看起来在尝试获取北京今天的天气信息时遇到了一些技术问题...
看第二行输出——qwen-max 自动识别出"查天气"意图,自动填好了 city_name='北京' 参数。这就是 Function Calling 的魅力:意图识别、参数抽取全由模型完成,代码只需要"执行"。
(注:示例中高德 key 未配置,工具返回了错误,但链路是通的。配置真实 key 后即可拿到温度、天气、风力等完整数据。)
七、踩坑记录(真实经历)
坑 1:DashScope 不支持 gpt-4o ❌
迁移时第一反应是"只改 key 就行",结果报模型不存在。DashScope 兼容层只认通义自家的模型名(qwen-plus / qwen-max / qwen-turbo)。教训:模型名必须同步替换。
坑 2:Windows 控制台中文乱码 ❌
运行日志全是 锟斤拷。原因:Windows cmd 默认 GBK 编码,Python 输出 UTF-8。解决:
python -X utf8 main.py # Python 3.7+ 可用
坑 3:Tavily 测试 key 超限 ❌
项目里 Tavily key 硬编码在代码里(tvly-dev-xxx),早就超了使用额度,搜索返回 usage limit。这暴露了一个架构问题——密钥不应硬编码在代码中,应统一收敛到 .env。
坑 4:依赖版本断档 ❌
requirements.txt 锁定 langchain==0.1.17,而环境里是 1.x。好在项目只用到了 ChatOpenAI / ChatPromptTemplate / StrOutputParser / bind_tools 这些跨版本稳定的核心 API,直接跑通。教训:锁定版本要慎重,过度锁定反而增加迁移成本。
八、架构点评:亮点与槽点
✅ 值得学习的亮点
- 分层清晰、依赖单向:
tools → agents → main,基础设施层独立 - 配置有校验:Pydantic 启动即校验,错误前置暴露
- 日志专业:四路输出 + 轮转 + 压缩,可直接上生产
- 工具返回结构统一:
{success, data, error}让上层零判断成本
⚠️ 可以改进的槽点
- 密钥硬编码:Tavily key 写死在工具类默认参数里,应收敛到
.env - 工具分发用 if/elif 写死:新增工具要改主流程代码,应改为"工具名 → 函数"映射表,或直接用 LangChain Agent 标准机制
- 多轮对话无记忆:
conversation_history一直在记录,却从未传入 LLM,"多轮"名不副实 - 命名不一致:
.env.example写的是和风天气QWEATHER_API_KEY,实际用的是高德AMAP_API_KEY——历史迭代没清理
九、总结与下一步
这个项目麻雀虽小,五脏俱全——配置校验、结构化日志、工具封装、Function Calling、国产模型迁移,一个 Agent 应用该有的要素都有了。如果你正在入门 LangChain,非常推荐照着这个结构搭一个自己的助手。
下一步可以玩的方向:
- 🔧 把 if/elif 分发改成工具映射表,支持热插拔新工具
- 🧠 把对话历史真正注入 LLM,实现多轮记忆
- 🌐 加一个 FastAPI 接口层,从 CLI 变成 Web 服务
- 📄 把知识库接进来(LlamaIndex + 切片 + RAG),升级成企业问答机器人
如果这篇文章对你有帮助,欢迎点赞、在看、转发,让更多人看到国产大模型 + LangChain 的实战姿势!
文中项目代码结构完整、可直接复现。关注公众号,回复"多任务助手"获取完整源码与配置说明。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)