实战:用 LangChain + 通义千问,打造一个能查天气、能搜新闻的多任务问答助手

前段时间拿到一个基于 LangChain 构建的多任务问答助手项目,本来跑的是 OpenAI 的 gpt-4o,在国内网络环境下体验不佳。索性花了一个晚上把它整体迁移到国产大模型——阿里云通义千问(qwen-max),把架构、代码、踩坑记录都整理了出来。这篇文章不讲大道理,全部是能跑通的代码和真实运行日志,希望对想入门 LangChain 工具调用(Function Calling)的同学有帮助。


为什么写这篇?

先看三个现状:

  1. LangChain 的教程多,但"完整可运行的项目"少——多数教程只讲一个 llm.invoke() 就结束了,工具调用、日志、配置管理这些工程细节全靠自己摸索。
  2. OpenAI 的项目在国内迁移有真实需求——API 访问不稳定、key 获取门槛高,而国产模型(通义千问)已经提供了 OpenAI 兼容接口,切换成本极低。
  3. "能查天气 + 能搜新闻"的 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})

用一张图理解整个调用流程:

不需要工具

需要工具

用户提问

LLM 判断

通用对话链
prompt | llm | output

解析 tool_calls

执行工具
天气 / 搜索

LLM 格式化工具结果

自然语言回答


五、国产化迁移:从 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,直接跑通。教训:锁定版本要慎重,过度锁定反而增加迁移成本。


八、架构点评:亮点与槽点

✅ 值得学习的亮点

  1. 分层清晰、依赖单向tools → agents → main,基础设施层独立
  2. 配置有校验:Pydantic 启动即校验,错误前置暴露
  3. 日志专业:四路输出 + 轮转 + 压缩,可直接上生产
  4. 工具返回结构统一{success, data, error} 让上层零判断成本

⚠️ 可以改进的槽点

  1. 密钥硬编码:Tavily key 写死在工具类默认参数里,应收敛到 .env
  2. 工具分发用 if/elif 写死:新增工具要改主流程代码,应改为"工具名 → 函数"映射表,或直接用 LangChain Agent 标准机制
  3. 多轮对话无记忆conversation_history 一直在记录,却从未传入 LLM,"多轮"名不副实
  4. 命名不一致.env.example 写的是和风天气 QWEATHER_API_KEY,实际用的是高德 AMAP_API_KEY——历史迭代没清理

九、总结与下一步

这个项目麻雀虽小,五脏俱全——配置校验、结构化日志、工具封装、Function Calling、国产模型迁移,一个 Agent 应用该有的要素都有了。如果你正在入门 LangChain,非常推荐照着这个结构搭一个自己的助手。

下一步可以玩的方向:

  • 🔧 把 if/elif 分发改成工具映射表,支持热插拔新工具
  • 🧠 把对话历史真正注入 LLM,实现多轮记忆
  • 🌐 加一个 FastAPI 接口层,从 CLI 变成 Web 服务
  • 📄 把知识库接进来(LlamaIndex + 切片 + RAG),升级成企业问答机器人

如果这篇文章对你有帮助,欢迎点赞、在看、转发,让更多人看到国产大模型 + LangChain 的实战姿势!

文中项目代码结构完整、可直接复现。关注公众号,回复"多任务助手"获取完整源码与配置说明。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐