拒绝黑盒:从零手写 Agent,彻底搞懂 LangChain、MCP 与 OpenClaw(你的第一个WeatherAgent智能体)
1. 引言
为什么要“手搓”一个 Agent智能体?
最近技术圈有个词很火——OpenClaw(Agent框架),大家都在聊 Agent,但有多少人真正理解其内部运作?
本文目标:不依赖现成黑盒,用几十行代码从零构建 WeatherAgent(天气查询Agent),在实战中拆解核心技术。
2. 理清概念
2.1 Function Calling
LLM 的“原生双手”,模型的原生能力,让 LLM 能输出结构化指令来请求外部工具,而不是只会接龙说话。就像给模型装了一双手,它能主动说"帮我查一下",而不是瞎编答案。没有 Function Calling,LLM 只能"瞎编";有了它,LLM 能"动手查"。
# 定义工具:LLM 的"手"
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "城市名,如北京"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
}
}
]
# LLM 决定"伸手"还是"动嘴"
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
tools=tools # 给模型装上"手"
)
# 模型输出结构化指令,而非纯文本
if response.choices[0].message.tool_calls:
# 模型说:"帮我查一下北京天气"
tool_call = response.choices[0].message.tool_calls[0]
print(tool_call.function.name) # get_weather
print(tool_call.function.arguments) # {"location": "北京", "unit": "celsius"}
可视化流程:
| 用户问"北京天气" ↓ LLM 思考:需要外部数据 → 生成 JSON 指令(Function Calling) ↓ 系统执行 get_weather("北京") → 返回结果 ↓ LLM 结合结果生成自然语言回答 |
2.2 LangChain
AI 开发的“乐高工厂”,AI 应用开发框架,把调用模型、管理状态、处理工具调用这些重复劳动封装成标准化模块。就像乐高工厂,你不用自己造积木,直接搭就行。LangChain 把"调用模型→解析输出→执行工具→管理上下文"这套脏活累活封装好了,你只需搭积木。
对比代码:不用 vs 用 LangChain
# ❌ 不用 LangChain:自己造轮子(50 行)
import openai
import os
from typing import List, Dict
def chat_with_tools(messages: List[Dict], tools: List[Dict]):
response = openai.chat.completions.create(
model="gpt-4",
messages=messages,
tools=tools
)
if response.choices[0].finish_reason == "tool_calls":
tool_call = response.choices[0].message.tool_calls[0]
result = execute_tool(tool_call.function.name,
json.loads(tool_call.function.arguments))
messages.append({"role": "tool", "content": str(result)})
return chat_with_tools(messages, tools) # 递归调用,自己管理状态
return response.choices[0].message.content
# ✅ 用 LangChain:5 行搞定
from langchain.agents import initialize_agent, Tool
from langchain_openai import ChatOpenAI
tools = [Tool(name="weather", func=get_weather, description="查天气")]
agent = initialize_agent(tools, ChatOpenAI(model="gpt-4"), agent="openai-tools")
result = agent.run("北京今天天气怎么样?") # 自动处理调用链、状态管理、错误重试
2.3 MCP
连接万物的“USB 协议”,AI 工具连接的行业协议,让任何工具都能以统一接口被任何 Agent 调用。就像 USB 标准,插上就能用,不用装专用驱动。以前每个工具要单独写适配器(装驱动),现在只要实现一次 MCP Server,任何 MCP Client(Claude、OpenClaw、Cursor)都能即插即用。
现在将你的接口或服务封装成 MCP Server,本质上就是将其“标准化”为一种 AI 原生(AI-Native)的通用插件格式。只需要写一套代码就可以通用了。
就像给手机充电。华为用一种口,苹果用 Lightning,老安卓用 Micro-USB。你想给不同手机充电,得找对应的线(写单独的适配器)。
现在(MCP): 就像 USB-C口统一了,不需要这么多接口的充电线了,只要大家都遵循 USB-C(MCP)标准,随便插哪都能用,即插即用。
MCP 配置文件示例(Claude Desktop 配置):
// claude_desktop_config.json
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/xxx/Desktop"],
"description": "文件系统访问 - USB 设备 A"
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxx"},
"description": "GitHub 操作 - USB 设备 B"
},
"sqlite": {
"command": "uvx",
"args": ["mcp-server-sqlite", "--db-path", "data.db"],
"description": "数据库查询 - USB 设备 C"
}
}
}
2.4 Skills
Agent 的"技能证书",Agent 能执行的具体能力单元,通常是一个函数或 API 的封装。查天气、发邮件、搜索网页,每个都是一个 Skill。
天气查询 Skill(Weather Query Skill):
文件结构:
weather-skill/
├── skill.md # 核心:人类可读,LLM 可执行
├── config.yaml # 配置:API 密钥、参数
├── handler.py # 可选:精确执行代码
└── test-cases.json # 测试用例
skill.md(核心文件)
---
name: weather
version: 1.2.0
type: skill
tags: [weather, utility]
icon: 🌤️
---
# 天气查询
## 描述
获取指定城市的实时天气和未来3天预报。
## 参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| city | string | ✅ | 城市名,如"北京"、"Shanghai" |
| unit | string | ❌ | 温度单位:celsius(默认)/fahrenheit |
## 执行步骤
1. **地理编码**:调用 `http.get("https://api.openweathermap.org/geo/1.0/direct?q={city}&limit=1&appid={env.WEATHER_API_KEY}")` 获取经纬度
2. **获取天气**:调用 `http.get("https://api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&units={unit}&appid={env.WEATHER_API_KEY}&lang=zh_cn")`
3. **解析数据**:
- 当前天气:取 `list[0]`
- 未来3天:取 `list[8], list[16], list[24]`(每8个3小时为一天)
4. **格式化输出**:
- 温度保留整数
- 天气描述翻译为中文
- 日期格式化为"周一/周二"等
## 输出格式
```json
{
"city": "城市名",
"current": {
"temp": "当前温度",
"condition": "天气状况",
"icon": "☀️"
},
"forecast": [
{"date": "周一", "high": "最高温", "low": "最低温", "condition": "多云"}
]
}
2.5 OpenClaw
开箱即用的"智能员工",近期爆火的开源 Agent 框架,在现有技术上做了更多产品化封装,让非开发者也能快速构建和分享智能体。本质是"开箱即用的 Agent 工具箱"。
| 维度 | AutoGPT | MetaGPT | Dify | OpenClaw |
|---|---|---|---|---|
| 定位 | 单 Agent 探索 | 软件开发自动化 | LLM 应用平台 | 个人 AI 助手运行时 |
| 使用难度 | 高 | 中 | 低 | 极低(对话式) |
| IM 集成 | 需自建 | 无 | 需配置 | 原生支持 15+ 平台 |
| 记忆能力 | 基础 | 无 | 中等 | OpenViking 长程记忆 |
| 适合谁 | 研究者 | 工程师 | 开发者 | 所有人 |
2.6 RAG(检索增强生成)
给 Agent 配个"图书馆",让模型先查资料再回答的技术方案。模型遇到不知道的问题,先去知识库里检索相关信息,再结合检索结果生成答案。解决的是"模型知识过时/私有数据访问"问题。开源的系统有ruoyi-ai,dify等
2.7 Workflow(工作流)
预设的任务执行流程,步骤固定、路径明确。比如"收到邮件→提取信息→存入数据库→发送通知",每一步都提前定义好。开源的系统有ruoyi-ai,dify等
2.8 Agent
能自主感知、规划、行动的智能体。和 Workflow 的区别在于:Workflow 是"按剧本演",Agent 是"自己写剧本自己演"。
2.9 总结
Function Calling 是地基,Agent 是房子,LangChain/OpenClaw 是施工队,MCP 是建材标准,Skills 是房间功能,RAG 是图书馆,Workflow 是另一种建筑风格。
| 概念 | 定位 | 必须吗? | 代码/配置占比 | 类比 |
|---|---|---|---|---|
| Function Calling | 模型原生能力 | ✅ 必须 | 20%(底层协议) | 人的"手" |
| Agent | 应用形态 | ✅ 核心目标 | 30%(编排逻辑) | "智能员工" |
| Skills | 能力单元 | ✅ 必须有 | 25%(业务逻辑) | 员工的"技能证书" |
| LangChain | 开发框架 | ❌ 可选 | 15%(胶水代码) | 工具台+乐高积木 |
| MCP | 连接协议 | ❌ 可选(趋势) | 5%(配置文件) | USB 接口标准 |
| OpenClaw | 产品化框架 | ❌ 可选 | 0%(开箱即用) | 精装公寓 |
| RAG | 知识增强 | ❌ 按需 | 10%(数据管道) | 员工的"参考书" |
| Workflow | 执行模式 | ❌ 并列 | 20%(流程定义) | "固定剧本"流 |
3.架构设计
3.1 架构分层

架构说明:
-
交互层:只负责输入/输出,不包含业务逻辑,切换界面不影响核心功能。
-
核心层:是 Agent 的“大脑+神经系统”,封装了 LLM、提示词、执行循环。
-
支撑层:记忆、工具、配置作为独立模块被核心层调用,实现关注点分离。
3.2 核心运行时序图

流程关键点说明:
-
上下文组装:核心层从记忆层获取历史,拼接到 Prompt 中,让模型拥有多轮对话能力。
-
模型决策:LLM 根据用户输入和工具描述,输出结构化的工具调用指令(Function Calling),而不是直接回答。
-
执行与回填:核心层解析指令,调用对应工具,将工具返回的真实数据再次发给模型。
-
最终生成:模型基于真实数据生成面向用户的友好回复,并存入记忆层。
4. 实战演练:从零构建 WeatherAgent
这一节,要像解剖一样,把每个文件拆开,告诉你:
- 这段代码在 Agent 里扮演什么角色?
- 为什么要这么写?换种写法行不行?
- 如果去掉这部分,Agent 会怎么样?
理解完这一章,你再看到任何 Agent 框架,都能一眼看穿它的"内脏结构"。

记住这个结构,后面每个文件的分析都能对应上。
4.1 环境准备
4.1.1 项目结构

这样拆分,是为了对应之前讲的核心概念:
tools/独立:对应 MCP 思想。工具是独立的插件,今天加天气,明天加搜索,不应该影响agent/的核心逻辑。工具就是"USB 设备",插上就能用。agent/core.py独立:对应 Function Calling 核心。这里是处理"模型思考→工具调用→结果回填"循环的地方,是 Agent 的"大脑皮层"。agent/memory.py独立:对应 状态管理。对话历史是独立的资源,方便后续替换成数据库或 Redis。main.pyvsweb_app.py:对应 交互层分离。核心逻辑不变,只是换了个界面(命令行 vs 网页)
4.1.2 创建Python环境
requirements.txt文件,项目当前所需要的环境依赖如下,也可以根据自己情况进行环境依赖的加减:
# 创建 requirements.txt
# LangChain
langchain==0.3.0
langchain-openai==0.2.0
langchain-community==0.3.0
python-dotenv==1.0.0
requests==2.31.0
langchain-core==0.3.0
langchain-ollama==0.2.0
# Web 界面
streamlit==1.32.0
# 其他
pydantic==2.10.0
4.1.3 创建DeepSeek API Key
在Deepseek开发者平台创建自己的key,也可以使用其他平台模型,也可以在本地利用ollama部署一个轻量模型,完全免费。

4.1.4 创建高德天气API
在高德开放平台控制台上创建自己的应用,然后添加key,绑定服务是web服务即可。

4.2 具体代码模块
4.2.1 配置层
永远不要把密钥硬编码在代码里。这是安全底线。同时,把配置抽离出来,意味着你切换模型(比如从 GPT-4 切换到 Claude)时,不需要修改任何业务代码,只改配置文件。你也可以用config进行配置都可以。
# .env 文件
#Deepseekkey
DEEPSEEK_API_KEY=sk-6d8934abb62844deb55bb826da....
#高德天气key
GAODE_API_KEY=61918bada8f60fb3a77a225a5b......
DEEPSEEK_BASE_URL=https://api.deepseek.com
4.2.2 核心层
agent/core.py(最重要)
"""
Agent 核心 - 使用 DeepSeek API
"""
from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain.prompts import ChatPromptTemplate
from typing import List
from dotenv import load_dotenv
import os
load_dotenv()
class WeatherAgent:
"""天气查询 Agent"""
def __init__(self, tools: List, model_name: str = "deepseek-chat"):
"""初始化 Agent"""
# DeepSeek API 配置(兼容 OpenAI 格式)
api_key = os.getenv("DEEPSEEK_API_KEY", "")
base_url = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
if not api_key:
raise ValueError("❌ DeepSeek API Key 未配置,请在 .env 文件中设置 DEEPSEEK_API_KEY")
# 使用 DeepSeek 模型
self.llm = ChatOpenAI(
model=model_name,
api_key=api_key,
base_url=base_url,
temperature=0.7,
)
# 创建提示词
self.prompt = ChatPromptTemplate.from_messages([
("system", """你是一个智能天气助手,可以帮助用户查询天气信息。
你可以使用以下工具:
- get_weather: 查询指定城市的实时天气(温度、湿度、风向等)
- web_search: 搜索网络信息
请用简洁友好的方式回复用户。如果查询到天气数据,请清晰展示并给出合理建议。"""),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
# 创建 Agent
self.agent = create_tool_calling_agent(
llm=self.llm,
tools=tools,
prompt=self.prompt
)
# 创建执行器
self.executor = AgentExecutor(
agent=self.agent,
tools=tools,
verbose=True,
max_iterations=5,
handle_parsing_errors=True
)
def chat(self, user_input: str) -> str:
"""与 Agent 对话"""
try:
result = self.executor.invoke({"input": user_input})
return result["output"]
except Exception as e:
return f"抱歉,处理您的请求时出错:{str(e)}"
def chat_with_memory(self, user_input: str, memory) -> str:
"""带记忆的对话"""
memory.add_message("user", user_input)
context = memory.get_context_string()
full_input = f"对话历史:\n{context}\n\n用户:{user_input}"
response = self.chat(full_input)
memory.add_message("assistant", response)
return response
agent/core.py —— Agent 的"大脑皮层"
代码作用:
定义 WeatherAgent 类,封装了 Agent 的核心运行逻辑。
关键代码段解析:
| 代码段 | 作用 | 为什么这么写 |
|---|---|---|
ChatOpenAI(...) |
初始化 LLM 客户端 | DeepSeek API 兼容 OpenAI 格式,所以用 langchain_openai 即可,不用单独 SDK |
ChatPromptTemplate.from_messages([...]) |
定义系统提示词 | 告诉模型"你是谁、有什么工具、怎么回复",这是 Agent 行为的"宪法" |
create_tool_calling_agent(...) |
创建工具调用 Agent | LangChain 封装好的函数,自动处理 Function Calling 的解析逻辑 |
AgentExecutor(...) |
创建执行器 | 负责"调用模型→解析工具请求→执行工具→回填结果"的完整循环 |
chat_with_memory(...) |
带记忆的对话方法 | 把记忆内容拼接到用户输入里,让模型能"记得之前说过什么" |
为什么要这么写?
- 类封装:把 Agent 封装成类,方便多次复用。如果写成函数,状态管理会很混乱。
- LangChain 封装:
create_tool_calling_agent和AgentExecutor是 LangChain 的核心抽象,处理了 Function Calling 的 JSON 解析、错误重试、循环控制等繁琐逻辑。 - 配置分离:API Key 从环境变量读取,不在代码里硬编码,安全且方便切换。
如果去掉这部分:
Agent 就没有"大脑"了,无法理解用户意图,无法决定调用哪个工具。
在 Agent 中的角色:
核心决策层 —— 决定"什么时候调用工具、调用哪个工具、怎么整合结果"
4.2.3 记忆层
agent/memory.py
"""
记忆系统 - Agent 的"大脑记忆"
"""
from typing import List
class AgentMemory:
"""Agent 记忆管理类"""
def __init__(self, max_messages: int = 10):
"""
初始化记忆
:param max_messages: 保留最近多少条对话
"""
self.messages: List[dict] = []
self.max_messages = max_messages
def add_message(self, role: str, content: str):
"""添加对话到记忆"""
self.messages.append({"role": role, "content": content})
# 保持消息数量不超过限制
if len(self.messages) > self.max_messages:
self.messages = self.messages[-self.max_messages:]
def get_history(self) -> List[dict]:
"""获取对话历史"""
return self.messages
def clear(self):
"""清空记忆"""
self.messages = []
def get_context_string(self) -> str:
"""获取记忆上下文(用于传给 LLM)"""
context = []
for msg in self.messages:
role_name = "用户" if msg["role"] == "user" else "助手"
context.append(f"{role_name}: {msg['content']}")
return "\n".join(context)
代码作用:
定义 AgentMemory 类,管理对话历史记录。
关键代码段解析:
| 代码段 | 作用 | 为什么这么写 |
|---|---|---|
self.messages: List[dict] = [] |
存储对话列表 | 用列表存历史,格式和 LLM API 要求的消息格式一致 |
max_messages 参数 |
限制记忆长度 | 防止 token 超标,同时控制成本(记忆越长,调用越贵) |
add_message() |
添加消息 | 统一入口,方便后续扩展(比如加时间戳、加摘要) |
get_context_string() |
获取上下文字符串 | 把记忆格式化成人类可读的文本,拼接到用户输入里 |
为什么要这么写?
- 独立类:记忆逻辑独立出来,方便后续替换。今天用内存列表,明天可以换成 Redis 或数据库。
- 长度限制:LLM 有 token 上限,记忆无限增长会导致请求失败。
max_messages是最简单的滑动窗口策略。 - 格式化输出:
get_context_string()把结构化数据转成文本,方便拼接到 Prompt 里。
如果去掉这部分:
Agent 就变成"金鱼记忆",每轮对话都是全新的,无法进行多轮交互。比如用户问"北京天气",再问"那上海呢",Agent 不知道"那"指的是什么。
在 Agent 中的角色:
状态管理层 —— 让 Agent 能"记住上下文",实现真正的对话而非单次问答
4.2.4 工具层
tools/weather_tool.py & tools/search_tool.py
"""
天气查询工具 - 使用高德地图真实天气 API
tools/weather_tool.py
"""
from langchain.tools import tool
import requests
import os
from dotenv import load_dotenv
load_dotenv()
# 高德天气 API Key
GAODE_API_KEY = os.getenv("GAODE_API_KEY", "")
@tool
def get_weather(city: str) -> str:
"""
查询指定城市的当前天气情况(使用高德地图 API)
Args:
city: 城市名称,如'北京'、'上海'、'广州'
Returns:
天气信息字符串
"""
if not GAODE_API_KEY:
return "❌ 高德 API Key 未配置,请在 .env 文件中设置 GAODE_API_KEY"
try:
# 1. 先获取城市编码(adcode)
geo_url = "https://restapi.amap.com/v3/config/district"
geo_params = {
"keywords": city,
"subdistrict": 0,
"key": GAODE_API_KEY
}
geo_response = requests.get(geo_url, params=geo_params, timeout=5)
geo_data = geo_response.json()
if geo_data.get("status") != "1":
return f"❌ 未找到城市'{city}',请检查城市名称"
city_code = geo_data["districts"][0]["adcode"]
city_name = geo_data["districts"][0]["name"]
# 2. 查询实时天气
weather_url = "https://restapi.amap.com/v3/weather/weatherInfo"
weather_params = {
"city": city_code,
"key": GAODE_API_KEY,
"extensions": "base" # base: 实时天气
}
weather_response = requests.get(weather_url, params=weather_params, timeout=5)
weather_data = weather_response.json()
if weather_data.get("status") == "1" and weather_data.get("lives"):
live = weather_data["lives"][0]
return (f"🌤️ {city_name}当前天气:{live['weather']},"
f"温度{live['temperature']}°C,"
f"湿度{live['humidity']}%,"
f"风向{live['winddirection']}风{live['windpower']}级,"
f"发布时间:{live['reporttime']}")
else:
return f"❌ 无法获取'{city}'的天气信息"
except requests.exceptions.Timeout:
return "❌ 请求超时,请检查网络连接"
except Exception as e:
return f"❌ 查询出错:{str(e)}"
@tool
def web_search(query: str) -> str:
"""
搜索网络信息(模拟版本)
Args:
query: 搜索关键词
Returns:
搜索结果字符串
"""
return f"🔍 搜索'{query}':建议使用搜索引擎获取最新信息。"
tools/weather_tool.py —— Agent 的"天气之手"
代码作用:
定义 get_weather 工具函数,对接高德地图真实天气 API。
关键代码段解析:
| 代码段 | 作用 | 为什么这么写 |
|---|---|---|
@tool 装饰器 |
标记为 LangChain 工具 | LangChain 自动解析函数签名和文档字符串,生成工具描述传给模型 |
| 函数文档字符串 | 工具描述 | 模型看不见代码实现,只看这个描述来决定是否调用 |
city: str 参数 |
参数定义 | 告诉模型需要传什么参数,类型是什么 |
| 两步 API 调用 | 先查城市编码再查天气 | 高德 API 需要城市 adcode,不能直接用城市名,这是 API 的设计限制 |
try-except 错误处理 |
捕获异常 | 网络请求可能失败,要给用户友好的错误提示,而不是堆栈跟踪 |
为什么要这么写?
@tool装饰器:这是 LangChain 的语法糖,自动把函数转换成工具对象,省去手动定义name、description、args_schema的麻烦。- 文档字符串要详细:模型靠这个判断"什么时候用这个工具"。写模糊了,模型可能该用时不用,不该用时乱用。
- 真实 API 对接:用高德 API 而不是模拟数据,让 Agent 能真正解决用户问题,而不是"玩具演示"。
如果去掉这部分:
Agent 就没办法查天气了,用户问天气只能靠模型瞎编(幻觉)。
在 Agent 中的角色:
能力执行层 —— 让 Agent 能"真正做事",而不是只会说话
"""
搜索工具
tools/search_tool.py
"""
from langchain.tools import BaseTool
from typing import Type, Optional
from pydantic import BaseModel, Field
class SearchInput(BaseModel):
"""搜索输入参数"""
query: str = Field(description="搜索关键词")
class SearchTool(BaseTool):
"""网络搜索工具"""
# ✅ 添加类型注解
name: str = "web_search"
description: str = "当天气工具无法使用时,使用此工具搜索网络信息"
args_schema: Type[BaseModel] = SearchInput
def _run(self, query: str) -> str:
"""执行搜索"""
return f"搜索'{query}'的相关信息:根据最新数据,该问题需要进一步查询..."
async def _arun(self, query: str) -> str:
return self._run(query)
tools/search_tool.py —— Agent 的"搜索之手"
代码作用:
定义 SearchTool 类,提供网络搜索能力(当前是模拟版本)。
关键代码段解析:
| 代码段 | 作用 | 为什么这么写 |
|---|---|---|
继承 BaseTool |
使用类方式定义工具 | 比 @tool 装饰器更灵活,可以自定义更多属性 |
args_schema: Type[BaseModel] |
参数 schema | 用 Pydantic 定义参数结构,LangChain 自动验证参数格式 |
_run() 和 _arun() |
同步和异步执行方法 | LangChain 要求两个都实现,支持同步和异步调用 |
4.2.5 入口层
main.py & web_app.py
"""
主程序入口 - 命令行版本(DeepSeek API + 高德天气)
"""
from agent.core import WeatherAgent
from agent.memory import AgentMemory
from tools.weather_tool import get_weather, web_search
def main():
"""主函数"""
print("=" * 60)
print("🌤️ 欢迎使用天气查询助手 Agent(DeepSeek API 版)")
print("=" * 60)
print("示例问题:")
print(" • 北京天气怎么样?")
print(" • 上海和广州哪个更热?")
print(" • 明天适合出门吗?")
print("=" * 60)
print("输入 'quit' 退出,'clear' 清空记忆\n")
# 1. 初始化工具
tools = [
get_weather,
web_search
]
# 2. 初始化 Agent
try:
agent = WeatherAgent(tools=tools)
except ValueError as e:
print(str(e))
print("请在 .env 文件中配置 DEEPSEEK_API_KEY")
return
# 3. 初始化记忆
memory = AgentMemory(max_messages=10)
# 4. 主循环
while True:
try:
user_input = input("👤 你:").strip()
if user_input.lower() in ["quit", "exit", "q"]:
print("👋 再见!")
break
if user_input.lower() == "clear":
memory.clear()
print("✅ 记忆已清空")
continue
if not user_input:
continue
print("🤖 Agent 思考中...", end="\r")
response = agent.chat_with_memory(user_input, memory)
print(" " * 30)
print(f"🤖 Agent: {response}\n")
except KeyboardInterrupt:
print("\n👋 再见!")
break
except Exception as e:
print(f"❌ 错误:{str(e)}\n")
if __name__ == "__main__":
main()
代码作用:
提供命令行交互界面,让用户能和 Agent 对话。
关键代码段解析:
表格
| 代码段 | 作用 | 为什么这么写 |
|---|---|---|
while True 循环 |
持续对话 | 实现多轮对话,用户不用每次重启程序 |
input() |
获取用户输入 | 最简单的交互方式 |
quit/clear 命令 |
特殊指令处理 | 给用户控制权,可以退出或清空记忆 |
try-except |
错误捕获 | 防止程序崩溃,给用户友好提示 |
为什么要这么写?
- 简单直接:命令行是最快的验证方式,不需要额外依赖。
- 交互命令:
clear命令让用户能主动清空记忆,调试更方便。 - 分离关注点:交互逻辑和核心逻辑分离,
main.py只负责"输入输出",不关心 Agent 怎么工作。
如果去掉这部分:
Agent 还能工作,但用户没法跟它交互(只能从代码里调用)。
在 Agent 中的角色:
交互层(命令行) —— 用户和 Agent 的"对话窗口"
"""
天气查询 Agent - Web 界面(Streamlit + DeepSeek API)
"""
import streamlit as st
from agent.core import WeatherAgent
from agent.memory import AgentMemory
from tools.weather_tool import get_weather, web_search
import time
# 页面配置
st.set_page_config(
page_title="🌤️ 天气查询助手",
page_icon="🌤️",
layout="wide",
initial_sidebar_state="expanded"
)
# 自定义 CSS 样式
st.markdown("""
<style>
.main-header {
font-size: 2.5rem;
font-weight: bold;
color: #1E88E5;
text-align: center;
padding: 1rem 0;
}
.chat-message {
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
max-width: 80%;
}
.user-message {
background-color: #E3F2FD;
margin-left: auto;
}
.assistant-message {
background-color: #F5F5F5;
margin-right: auto;
}
.thinking {
color: #666;
font-style: italic;
}
</style>
""", unsafe_allow_html=True)
# 标题
st.markdown('<p class="main-header">🌤️ 天气查询助手 Agent</p>', unsafe_allow_html=True)
st.markdown("---")
# 侧边栏
with st.sidebar:
st.header("⚙️ 设置")
# API 状态检查
st.markdown("**API 状态**:")
import os
from dotenv import load_dotenv
load_dotenv()
deepseek_key = os.getenv("DEEPSEEK_API_KEY", "")
gaode_key = os.getenv("GAODE_API_KEY", "")
if deepseek_key:
st.success("✅ DeepSeek API 已配置")
else:
st.error("❌ DeepSeek API 未配置")
if gaode_key:
st.success("✅ 高德天气 API 已配置")
else:
st.error("❌ 高德天气 API 未配置")
st.markdown("---")
# 清空记忆按钮
if st.button("🗑️ 清空对话记忆", use_container_width=True):
if "memory" in st.session_state:
st.session_state.memory.clear()
if "messages" in st.session_state:
st.session_state.messages = []
if "agent" in st.session_state:
del st.session_state.agent
st.rerun()
st.markdown("---")
st.markdown("**示例问题**:")
st.markdown("- 北京天气怎么样?")
st.markdown("- 上海和广州哪个更热?")
st.markdown("- 今天适合出门吗?")
st.markdown("---")
st.markdown("**关于**:")
st.markdown("基于 LangChain + DeepSeek + 高德天气 API 构建的智能天气助手")
# 初始化 Session State
if "memory" not in st.session_state:
st.session_state.memory = AgentMemory(max_messages=10)
if "messages" not in st.session_state:
st.session_state.messages = []
if "agent" not in st.session_state:
try:
# 初始化工具
tools = [get_weather, web_search]
# 初始化 Agent
st.session_state.agent = WeatherAgent(tools=tools)
except ValueError as e:
st.error(str(e))
st.stop()
# 显示历史消息
for message in st.session_state.messages:
if message["role"] == "user":
st.markdown(
f'<div class="chat-message user-message">👤 {message["content"]}</div>',
unsafe_allow_html=True
)
else:
st.markdown(
f'<div class="chat-message assistant-message">🤖 {message["content"]}</div>',
unsafe_allow_html=True
)
# 聊天输入框
st.markdown("---")
user_input = st.chat_input("输入你的问题,例如:上海天气怎么样?")
# 处理用户输入
if user_input:
# 显示用户消息
st.session_state.messages.append({"role": "user", "content": user_input})
st.markdown(
f'<div class="chat-message user-message">👤 {user_input}</div>',
unsafe_allow_html=True
)
# 显示思考中状态
thinking_placeholder = st.empty()
thinking_placeholder.markdown('<p class="thinking">🤖 Agent 思考中...</p>', unsafe_allow_html=True)
# 获取 Agent 回复
try:
with st.spinner("正在查询天气信息..."):
response = st.session_state.agent.chat_with_memory(
user_input,
st.session_state.memory
)
# 移除思考状态
thinking_placeholder.empty()
# 显示 Agent 回复
st.session_state.messages.append({"role": "assistant", "content": response})
st.markdown(
f'<div class="chat-message assistant-message">🤖 {response}</div>',
unsafe_allow_html=True
)
except Exception as e:
thinking_placeholder.empty()
st.error(f"❌ 出错:{str(e)}")
st.session_state.messages.append({"role": "assistant", "content": f"抱歉,出错了:{str(e)}"})
# 底部
st.markdown("---")
st.markdown(
"<p style='text-align: center; color: #666; font-size: 0.9rem;'>Powered by LangChain + DeepSeek + 高德天气 API</p>",
unsafe_allow_html=True
)
web_app.py —— Agent 的"图形化嘴巴"
代码作用:
使用 Streamlit 构建 Web 界面,提供更友好的交互体验。
关键代码段解析:
表格
| 代码段 | 作用 | 为什么这么写 |
|---|---|---|
st.session_state |
会话状态管理 | Web 是无状态的,需要用 session_state 保存记忆和消息历史 |
st.chat_input() |
聊天输入框 | Streamlit 原生组件,专为聊天界面设计 |
st.markdown(..., unsafe_allow_html=True) |
自定义样式 | 让消息气泡更像聊天软件,提升用户体验 |
| 侧边栏设置 | 配置和状态显示 | 让用户能看到 API 状态、清空记忆、看示例问题 |
为什么要用 Streamlit?
- 快速原型:几行代码就能搭建 Web 界面,适合演示和测试。
- 状态管理:
session_state天然适合保存对话历史。 - 美观:比命令行更友好,适合给非技术人员使用。
如果去掉这部分:
Agent 还能用命令行访问,但没有图形界面,用户体验差一些。
在 Agent 中的角色:
交互层(Web) —— 另一种"对话窗口",核心逻辑和命令行版本完全一样
4.3 启动
streamlit run web_app.py
也可以启动main.py文件,在命令行中进行测试。
4.4 运行效果

5. Agent 组成要素总结
| 组成部分 | 对应文件 | 核心作用 | 可否省略 | 类比 |
|---|---|---|---|---|
| 模型层 | (DeepSeek API) | 理解意图、做决策 | ❌ 不可省 | 大脑 |
| 核心层 | agent/core.py |
组织思考→工具→结果的循环 | ❌ 不可省 | 神经系统 |
| 记忆层 | agent/memory.py |
记住对话历史 | ⚠️ 可省(单轮对话) | 短期记忆 |
| 工具层 | tools/*.py |
执行具体任务 | ⚠️ 可省(纯聊天) | 双手 |
| 交互层 | main.py / web_app.py |
用户输入输出 | ⚠️ 可省(API 服务) | 嘴巴/耳朵 |
| 配置层 | .env |
存储敏感信息 | ❌ 不可省 | 身份证 |
Agent = 模型(大脑)+ 工具(双手)+ 记忆(记忆)+ 循环逻辑(神经系统)
架构与流程,才是 Agent 的“灵魂”
6.引申问题
6.1 工具层本质上就是 Skills 的具体实现
get_weather 函数,其实就是一个标准的 Skill 实现,当前的 Agent 技术生态中,“Skill”和“Tool”这两个词经常混用,但可以这样理解它们的关系:
-
Tool(工具):是代码层面的实现单元,就是你博客里
tools/weather_tool.py中定义的函数或类 -
Skill(技能):是更高层次的概念,指 Agent 具备的“一项能力”。一个 Skill 可能包含一个或多个 Tool,也可能包含提示词、元数据、使用示例等。
如何把我的工具层包装成Skills?
方案一:保持 LangChain 风格,但按 Skill 概念组织
这种方式的本质是将工具按“能力域”打包,每个 Skill 目录是自包含的,方便复用和分享
skills/ # 将 tools/ 重命名为 skills/
├── weather/
│ ├── __init__.py
│ ├── skill.py # 包含 get_weather 函数
│ ├── prompt.py # 该技能专用的提示词片段(可选)
│ └── config.py # 技能专属配置(如高德 API Key)
├── search/
│ ├── __init__.py
│ └── skill.py
└── __init__.py # 统一导出所有 skills
方案二:转换为 MCP 协议标准的 Skill
如果想让天气能力能被任何支持 MCP 的 Agent 框架(包括 OpenClaw、Claude Desktop、Cursor 等)调用,可以把它封装成一个 MCP Server。这是当前最“标准化”的做法。
步骤概览:
-
创建一个 Python 项目,定义 MCP Server
-
将
get_weather函数包装成 MCP 的 Tool 资源 -
通过 stdio 或 SSE 暴露服务
# weather_mcp_server.py
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions
import mcp.server.stdio
import mcp.types as types
import requests
import os
server = Server("weather-server")
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
return [
types.Tool(
name="get_weather",
description="查询指定城市的当前天气",
inputSchema={
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"}
},
"required": ["city"]
}
)
]
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent]:
if name == "get_weather":
city = arguments.get("city")
# 这里调用你原来的 get_weather 核心逻辑
weather_data = get_weather_from_amap(city)
return [types.TextContent(type="text", text=weather_data)]
raise ValueError(f"Unknown tool: {name}")
async def main():
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="weather-server",
server_version="1.0.0"
)
)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
方案三:转换为 OpenClaw 的 Skill 格式
如果正在使用或计划使用 OpenClaw 框架,它有自己的一套 Skill 规范。通常 OpenClaw 的 Skill 是一个目录,包含:
-
SKILL.md:描述技能用途、触发词、使用示例 -
run.py或main.py:执行入口 -
config.json:元数据
将天气功能转换后,目录结构类似:
skills/weather-skill/
├── SKILL.md # 文档,说明技能能做什么
├── run.py # 包含你的 get_weather 调用逻辑
└── config.json # 技能配置
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)