实战多功能智能助手
实战多功能智能助手
把前面的知识拼成一个能用的助手:五个工具(天气/计算/时间/货币/搜索)、用类封装 Agent、维护对话历史,以及自动演示+交互式主程序
前几篇把 Agent 的零件都拆开讲完了:创建、传模型、绑工具、写提示词、结构化输出、流式输出。这一篇把它们拼成一个能用的东西——多功能智能助手。
项目需求
开发一个多功能智能助手,支持:
- 天气查询:查询城市天气
- 数学计算:复杂数学运算
- 时间查询:获取当前时间、日期计算
- 货币转换:多种货币之间转换
- 信息搜索:搜索产品、新闻等信息
代码分成四步,正好对应我们前面学过的内容:
| 步骤 | 对应知识点 |
|---|---|
| 1. 模型的初始化 | 模型的创建 |
| 2. 工具的定义 | Tools工具定义与调用 |
| 3. Agent 的创建(面向对象封装) | 创建第一个智能体 |
| 4. 主程序(演示 + 交互) | 这一篇的重点 |
第 1 步:模型初始化
import osfrom dotenv import load_dotenvfrom langchain.chat_models import init_chat_model
load_dotenv(override=True)
model = init_chat_model( model="deepseek-v4-flash", model_provider="openai", api_key=os.getenv("DEEPSEEK_API_KEY"), base_url=os.getenv("DEEPSEEK_BASE_URL"),)第 2 步:定义五个工具
每个工具都只做一件事,并用规范的 docstring 写清 Args / Returns / Examples——这些文字就是模型选择工具、填参数的依据。
| 工具 | 参数 | 返回什么 |
|---|---|---|
get_weather(city) | 城市名称 | 温度、天气状况、空气质量 |
calculator(expression) | 数学表达式 | 计算结果 |
get_time_info(query_type) | current/date/tomorrow/yesterday/weekday | 时间信息 |
convert_currency(amount, from_curr, to_curr) | 金额 + 源/目标货币 | 转换结果 |
search_info(keyword, category) | 关键词 + 分类 | 产品/新闻搜索结果 |
几个值得学的写法:
from langchain_core.tools import toolimport mathfrom datetime import datetime, timedelta
@tooldef get_weather(city: str) -> str: """获取指定城市的实时天气信息
支持中国主要城市的天气查询
Args: city: 城市名称,如"北京"、"上海"、"深圳"等
Returns: 包含温度、天气状况、空气质量的详细信息
Examples: get_weather("北京") 返回 "多云,15-22℃,空气质量良" """ weather_db = { "北京": "多云,15-22℃,空气质量良,湿度 45%", "上海": "晴天,18-25℃,空气质量优,湿度 60%", "深圳": "小雨,22-28℃,空气质量优,湿度 75%", } result = weather_db.get(city) if result: return f"{city}:{result}" return f"抱歉,暂不支持查询{city}的天气信息。当前支持:北京、上海、深圳"① 查不到时要给出”可用的范围”——别只返回”没有数据”,模型没法据此修正。
@tooldef calculator(expression: str) -> str: """执行数学计算
支持基本运算符(+、-、*、/、**)和常用数学函数
Args: expression: 数学表达式,可以包含: - 基本运算:2 + 3, 10 * 5, 100 / 4 - 幂运算:2 ** 10 - 函数:sqrt(16), abs(-5), pow(2, 3)
Returns: 计算结果或错误信息
Examples: calculator("2 + 3 * 4") 返回 "14" calculator("sqrt(16)") 返回 "4.0" """ try: safe_functions = { "sqrt": math.sqrt, "pow": pow, "abs": abs, "round": round, "sin": math.sin, "cos": math.cos, "tan": math.tan, "log": math.log, "pi": math.pi, "e": math.e, } result = eval(expression, {"__builtins__": {}}, safe_functions) return f"{expression} = {result}" except Exception as e: return f"计算出错:{str(e)}\n提示:请检查表达式格式,支持的函数有 sqrt, abs, pow, sin, cos, tan, log"② eval 要”锁死”环境:第二个参数传 {"__builtins__": {}}(禁用内置函数),第三个参数只放白名单数学函数——这是让模型能算数、又不至于执行任意代码的常见做法。
@tooldef get_time_info(query_type: str = "current") -> str: """获取时间相关信息
Args: query_type: 查询类型 - "current": 当前时间 - "date": 今天日期 - "tomorrow": 明天日期 - "yesterday": 昨天日期 - "weekday": 星期几
Returns: 时间信息字符串
Examples: get_time_info("current") 返回 "2025年1月25日 14:30:25" get_time_info("weekday") 返回 "星期六" """ now = datetime.now() if query_type == "current": return now.strftime("当前时间:%Y年%m月%d日 %H:%M:%S") elif query_type == "date": return now.strftime("今天是:%Y年%m月%d日") elif query_type == "tomorrow": return (now + timedelta(days=1)).strftime("明天是:%Y年%m月%d日") elif query_type == "yesterday": return (now - timedelta(days=1)).strftime("昨天是:%Y年%m月%d日") elif query_type == "weekday": weekdays = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] return f"今天是{weekdays[now.weekday()]}" return f"不支持的查询类型:{query_type}。支持:current, date, tomorrow, yesterday, weekday"③ 用”枚举式参数”代替多个工具:一个 get_time_info 顶五个时间工具——工具越少,模型越不容易选错(第 14 篇的常见问题)。
@tooldef convert_currency(amount: float, from_curr: str, to_curr: str) -> str: """货币转换工具 支持主要货币之间的实时汇率转换
Args: amount: 金额数值 from_curr: 源货币代码(CNY/USD/EUR/GBP/JPY/HKD) to_curr: 目标货币代码(CNY/USD/EUR/GBP/JPY/HKD)
Returns: 转换结果
Examples: convert_currency(100, "CNY", "USD") 返回 "100 CNY = 14.00 USD" """ exchange_rates = {"CNY": 1.0, "USD": 0.14, "EUR": 0.13, "GBP": 0.11, "JPY": 20.8, "HKD": 1.09} currency_names = {"CNY": "人民币", "USD": "美元", "EUR": "欧元", "GBP": "英镑", "JPY": "日元", "HKD": "港币"}
from_curr, to_curr = from_curr.upper(), to_curr.upper() if from_curr not in exchange_rates: return f"不支持的源货币:{from_curr}。支持的货币:CNY, USD, EUR, GBP, JPY, HKD" if to_curr not in exchange_rates: return f"不支持的目标货币:{to_curr}。支持的货币:CNY, USD, EUR, GBP, JPY, HKD"
cny_amount = amount / exchange_rates[from_curr] # 先换成 CNY 再换目标货币 result_amount = cny_amount * exchange_rates[to_curr] return f"{amount} {currency_names[from_curr]}({from_curr})= {result_amount:.2f} {currency_names[to_curr]}({to_curr})"@tooldef search_info(keyword: str, category: str = "all") -> str: """搜索各类信息
Args: keyword: 搜索关键词 category: 搜索分类 - "product": 搜索产品 - "news": 搜索新闻 - "all": 搜索所有
Returns: 搜索结果 """ products = { "手机": "iPhone 15 (¥5999), 小米14 (¥3999), 华为Mate60 (¥6999)", "笔记本": "MacBook Pro (¥12999), ThinkPad X1 (¥9999), 华为MateBook (¥7999)", "耳机": "AirPods Pro (¥1999), Sony WH-1000XM5 (¥2499)", } news = { "AI": "1. GPT-5 即将发布 2. AI 芯片市场增长 30% 3. 新AI法规出台", "科技": "1. 量子计算新突破 2. 6G 技术测试 3. 新能源汽车销量创新高", }
results = [] if category in ["product", "all"]: for key, value in products.items(): if keyword in key: results.append(f"【产品】{key}:{value}") if category in ["news", "all"]: for key, value in news.items(): if keyword in key or keyword in value: results.append(f"【新闻】{key} 相关:{value}")
return "\n".join(results) if results else f"未找到关于 '{keyword}' 的{category}信息"注意 search_info 的 category: str = "all" 和 get_time_info 的 query_type: str = "current"——参数带默认值时,模型可以不传(第 11 篇的”默认值”特性在工具里同样适用)。
第 3 步:用类封装 Agent
这一步是实战精华:把”模型 + 工具 + 提示词 + 对话历史”打包成一个类,对外只暴露 chat() 和 reset()。
from langchain.agents import create_agent
class SmartAssistant: """多功能智能助手"""
def __init__(self): self.model = model
self.tools = [get_weather, calculator, get_time_info, convert_currency, search_info]
system_prompt = """你是一个多功能智能助手,可以帮助用户:
🌤️ 查询天气:使用 get_weather 工具 🔢 数学计算:使用 calculator 工具 ⏰ 时间查询:使用 get_time_info 工具 💱 货币转换:使用 convert_currency 工具 🔍 信息搜索:使用 search_info 工具
重要提示: 1. 仔细阅读用户问题,确定需要使用哪个工具 2. 如果需要多个工具,按顺序调用 3. 总是用友好、专业的语气回答 4. 如果工具返回了数据,要用通俗易懂的语言解释给用户 5. 如果无法完成任务,诚实地告诉用户原因
请始终使用中文回答。"""
self.agent = create_agent( model=self.model, tools=self.tools, system_prompt=system_prompt, )
self.messages = [] # 对话历史
def chat(self, user_input: str) -> str: """对话接口""" self.messages.append({"role": "user", "content": user_input}) # 1. 追加用户消息
result = self.agent.invoke({"messages": self.messages}) # 2. 调用 agent(带上历史)
self.messages = result["messages"] # 3. 用返回的完整消息列表更新历史
for msg in reversed(self.messages): # 4. 从后往前找最后一条有内容的 AI 消息 if msg.type == "ai" and msg.content: return msg.content
return "抱歉,我无法处理这个请求。"
def reset(self): """重置对话历史""" self.messages = []这段代码里有四个必须理解的细节:
| 细节 | 为什么这么写 |
|---|---|
self.messages 一直保留 | Agent 本身不带记忆,想让它”记得上文”就得每次把历史一起传进去 |
用 result["messages"] 覆盖历史 | 返回的是含工具调用全过程的完整列表,覆盖后才和模型看到的上下文一致 |
reversed(...) 从后往前找 | 最后一条消息不一定是”有内容的 AI 消息”(可能是工具调用的空 content) |
msg.content 判空 | AIMessage 在”发起工具调用”时 content 是空的,不能直接当回答返回 |
再加上第 17 篇的流式输出,就能把这个助手升级成”打字机式”回复——chat() 里把 invoke 换成 stream(..., stream_mode="messages") 再逐片打印即可。
第 4 步:主程序
def main(): assistant = SmartAssistant()
print("=" * 40) print("🤖 多功能智能助手(LangChain 1.2)") print("=" * 40) print("\n输入 'quit' 退出,输入 'reset' 重置对话\n")
# 先自动跑几条演示,确认每个工具都能被正确调用 demos = [ "北京今天天气怎么样?", "帮我算一下 (25 + 17) * 3", "现在几点了?", "100 美元等于多少人民币?", ]
for demo in demos: print(f"👤 {demo}") response = assistant.chat(demo) print(f"🤖 {response}\n")
assistant.reset()
# 交互模式 while True: user_input = input("\n👤 你: ") if user_input.lower() == "quit": print("再见!👋") break if user_input.lower() == "reset": assistant.reset() print("✅ 对话已重置") continue if not user_input.strip(): continue print(f"🤖 助手: {assistant.chat(user_input)}")
if __name__ == "__main__": main()设计上值得抄的两点:
demos自动演示:每次运行先把每个工具跑一遍——既是演示,也是”冒烟测试”,一眼看出哪个工具挂了quit/reset指令:一个退出、一个清空对话历史;reset在调试”多轮对话跑偏”时特别有用
跑起来长什么样
课程运行后打出来的完整输出(2026-06-03 那次运行,四条 demo 都命中了工具):
========================================🤖 多功能智能助手(LangChain 1.2)========================================
我可以帮你:🌤️ 查询天气🔢 数学计算⏰ 时间查询💱 货币转换🔍 信息搜索
输入 'quit' 退出,输入 'reset' 重置对话
👤 北京今天天气怎么样?🤖 北京今天天气是:**多云,15–22℃**,**空气质量良**,湿度 **45%**。整体来说比较适合外出,建议穿**轻薄外套**会更舒服。
👤 帮我算一下 (25 + 17) * 3🤖 计算结果是:**126**。
👤 现在几点了?🤖 现在是:**2026年06月03日 19:45:40**。
👤 100 美元等于多少人民币?🤖 **100 美元(USD)约等于 714.29 人民币(CNY)**。
========================================🤖 进入交互模式========================================再见!👋输出里”进入交互模式”那一行的 emoji 在课程 PDF 里没能还原出来(🤖 / 💬 都可能),不影响任何逻辑——照上面主程序的写法跑一遍,自己看到的就是真实的。
对照代码能看到几件事:
| 输出片段 | 说明 |
|---|---|
🤖 北京今天天气是:**多云,15–22℃**…建议穿**轻薄外套** | 工具返回的是原始数据(多云,15-22℃,空气质量良,湿度 45%),最后这句”建议穿轻薄外套”是模型自己加的——这就是系统提示词里”用通俗易懂的语言解释给用户”的效果 |
计算结果是:**126** | (25+17)*3 = 126,calculator 工具算对了(工具返回是 (25 + 17) * 3 = 126,模型复述时只留了数字) |
100 美元(USD)约等于 714.29 人民币(CNY) | 用 convert_currency 的汇率表手算能对上:100 / 0.14 × 1.0 = 714.29 |
末尾的 再见!👋 | 进入交互模式后直接输入 quit 的退出语 |
上面贴的是精简版主程序的运行效果。课程原版 main() 在循环前后各多打印了一块内容:
print("\n我可以帮你:")print(" 🌤️ 查询天气")print(" 🔢 数学计算")print(" ⏰ 时间查询")print(" 💱 货币转换")print(" 🔍 信息搜索")...print("=" * 40)print("🤖 进入交互模式")print("=" * 40)也就是输出里”我可以帮你:“那一段和”进入交互模式”前后的两条分隔线——想让界面更像”产品”,照抄这两块即可(不影响任何逻辑)。
相关
练习题
一、回忆填空(写完再展开对答案)
- 这个项目的四步结构:模型初始化 → ____的定义 → ____的创建(用类封装)→ 主程序
- 工具查不到数据时,最好在返回值里给出____,方便模型修正参数
calculator用eval时把内置函数锁死:eval(expression, {"____": {}}, safe_functions)- 用”枚举式参数”(如 time 工具的
query_type)代替多个工具的好处是:工具越____,模型越不容易____ SmartAssistant里self.messages的作用是保存____——因为 Agent 本身____记忆chat()里用result["____"]覆盖历史,保证上下文与模型看到的一致- 取回答时用
reversed()从后往前找msg.type == "____" and msg.____——因为发起工具调用时 AI 消息的 content 是____ - 主程序的
demos列表既做演示,也相当于一次”测试”;reset指令的作用是清空
填空答案(做完再点开)
- 工具 / Agent 2. 可用的范围(支持哪些) 3.
__builtins__4. 少 / 选错 5. 对话历史 / 不带 6.messages7.ai/content/ 空的 8. 冒烟 / 对话历史
二、裸写题
-
2-1 给助手加第 6 个工具 新增一个”汇率查询/单位换算/翻译”之类的工具(自己选),按
Args/Returns/Examples规范写 docstring,加进self.tools,然后问一个只有新工具能回答的问题,确认它被正确调用。提示(先自己想,实在想不出再点开)一级 · 思路:工具”能被正确调用”的唯一依据是 docstring 写得清楚 二级 · 方法:
@tool+ 写全 Args/Returns/Examples + 加进 tools 列表 三级 · 骨架:先把新工具单独invoke一次确认逻辑没问题,再交给 Agent -
2-2 把回复升级成流式打字机 修改
chat():用self.agent.stream({"messages": self.messages}, stream_mode="messages")逐片打印,并把最后一条消息也更新回self.messages,保证下一轮还能记得上下文。提示一级 · 思路:stream 的返回不给完整状态,所以”更新历史”要另想办法 二级 · 方法:
ASTREAM/stream里用chunk[0].content打印;历史更新可以先用invoke的返回,或者手动把最终回答拼成AIMessage追加 三级 · 骨架:偷懒的办法是”先 stream 显示、再 invoke 一次取完整结果”(多花一次调用,但代码最简单)
三、综合题
-
3-1 把 SmartAssistant 跑起来并读懂它
- 把第 1-4 步的代码拼成一个完整脚本(
.env里配好密钥),运行,确认 4 条演示都能得到正确回答 - 进入交互模式,先问”北京天气怎么样”,再问”那杭州呢”,观察第二轮能否理解”那……呢”指的是天气
- 输入
reset后再问”那杭州呢”,看它是不是失去了上下文 - 在
chat()里加一行print(len(self.messages)),观察每次问答后消息列表涨了几条(想想为什么会涨这么多)
提示(先自己想,实在想不出再点开)一级 · 思路:这道题的重点是”看见历史是怎么起作用的” 二级 · 方法:多轮对话能否连贯,全靠
self.messages里带上文 三级 · 骨架:消息列表每轮会新增 用户消息 + AI 工具调用 + 工具返回 + 最终回答,条数增长很快——这正是第 9 章要解决的”上下文太长”问题 - 把第 1-4 步的代码拼成一个完整脚本(
参考答案(做完再点开)
# ==================== 完整脚本(存成一个 .py 文件运行)====================import mathimport osfrom datetime import datetime, timedelta
from dotenv import load_dotenvfrom langchain.agents import create_agentfrom langchain.chat_models import init_chat_modelfrom langchain_core.tools import tool
load_dotenv(override=True)
model = init_chat_model( model="deepseek-v4-flash", model_provider="openai", api_key=os.getenv("DEEPSEEK_API_KEY"), base_url=os.getenv("DEEPSEEK_BASE_URL"),)
# ---------- 工具定义(4 个,压缩版)----------@tooldef get_weather(city: str) -> str: """获取指定城市的实时天气信息
Args: city: 城市名称,如"北京"、"上海"、"深圳"等
Returns: 温度、天气状况、空气质量 """ weather_db = {"北京": "多云,15-22℃,空气质量良", "杭州": "晴天,17-24℃,空气质量优"} return f"{city}:{weather_db[city]}" if city in weather_db else f"暂不支持{city},当前支持:北京、杭州"
@tooldef calculator(expression: str) -> str: """执行数学计算
Args: expression: 数学表达式,如 "2 + 3 * 4"、"sqrt(16)" """ safe = {"sqrt": math.sqrt, "pow": pow, "abs": abs, "round": round, "pi": math.pi, "e": math.e} try: return f"{expression} = {eval(expression, {'__builtins__': {}}, safe)}" except Exception as e: return f"计算出错:{e}"
@tooldef get_time_info(query_type: str = "current") -> str: """获取时间相关信息
Args: query_type: current 当前时间 / date 今天日期 / tomorrow 明天 / weekday 星期几 """ now = datetime.now() if query_type == "date": return now.strftime("今天是:%Y年%m月%d日") if query_type == "tomorrow": return (now + timedelta(days=1)).strftime("明天是:%Y年%m月%d日") if query_type == "weekday": return f"今天是{['星期一','星期二','星期三','星期四','星期五','星期六','星期日'][now.weekday()]}" return now.strftime("当前时间:%Y年%m月%d日 %H:%M:%S")
@tooldef convert_currency(amount: float, from_curr: str, to_curr: str) -> str: """货币转换
Args: amount: 金额 from_curr: 源货币代码(CNY/USD/EUR/GBP/JPY/HKD) to_curr: 目标货币代码(CNY/USD/EUR/GBP/JPY/HKD) """ rates = {"CNY": 1.0, "USD": 0.14, "EUR": 0.13, "GBP": 0.11, "JPY": 20.8, "HKD": 1.09} from_curr, to_curr = from_curr.upper(), to_curr.upper() if from_curr not in rates or to_curr not in rates: return "不支持的货币代码。支持:CNY, USD, EUR, GBP, JPY, HKD" result = amount / rates[from_curr] * rates[to_curr] return f"{amount} {from_curr} = {result:.2f} {to_curr}"
# ---------- 用类封装 ----------class SmartAssistant: """多功能智能助手"""
def __init__(self): self.tools = [get_weather, calculator, get_time_info, convert_currency] self.agent = create_agent( model=model, tools=self.tools, system_prompt=( "你是一个多功能智能助手。请先判断该用哪个工具,再调用它;" "如果需要多个工具,按顺序调用;用友好专业的语气、通俗易懂的语言回答;" "无法完成时如实说明原因。请始终使用中文回答。" ), ) self.messages = []
def chat(self, user_input: str) -> str: self.messages.append({"role": "user", "content": user_input}) result = self.agent.invoke({"messages": self.messages}) self.messages = result["messages"] print(f" [消息列表现在有 {len(self.messages)} 条]") # 3-1 第 4 问 for msg in reversed(self.messages): if msg.type == "ai" and msg.content: return msg.content return "抱歉,我无法处理这个请求。"
def reset(self): self.messages = []
# ---------- 主程序 ----------def main(): assistant = SmartAssistant()
for demo in ["北京今天天气怎么样?", "帮我算一下 (25 + 17) * 3", "现在几点了?", "100 美元等于多少人民币?"]: print(f"👤 {demo}") print(f"🤖 {assistant.chat(demo)}\n")
print("👤 那杭州呢?") # 追问:应该仍然是在问天气 print(f"🤖 {assistant.chat('那杭州呢?')}\n")
assistant.reset() print("👤 那杭州呢?(reset 之后)") print(f"🤖 {assistant.chat('那杭州呢?')}\n")
if __name__ == "__main__": main()评论区
如果你喜欢,那么欢迎来到我的世界!
了解更多













