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 核心运行时序图

流程关键点说明

  1. 上下文组装:核心层从记忆层获取历史,拼接到 Prompt 中,让模型拥有多轮对话能力。

  2. 模型决策:LLM 根据用户输入和工具描述,输出结构化的工具调用指令(Function Calling),而不是直接回答。

  3. 执行与回填:核心层解析指令,调用对应工具,将工具返回的真实数据再次发给模型。

  4. 最终生成:模型基于真实数据生成面向用户的友好回复,并存入记忆层。

4. 实战演练:从零构建 WeatherAgent

        这一节,要像解剖一样,把每个文件拆开,告诉你:

  1. 这段代码在 Agent 里扮演什么角色?
  2. 为什么要这么写?换种写法行不行?
  3. 如果去掉这部分,Agent 会怎么样?

        理解完这一章,你再看到任何 Agent 框架,都能一眼看穿它的"内脏结构"。

        记住这个结构,后面每个文件的分析都能对应上。

4.1 环境准备

4.1.1 项目结构

这样拆分,是为了对应之前讲的核心概念:

  1. tools/ 独立:对应 MCP 思想。工具是独立的插件,今天加天气,明天加搜索,不应该影响 agent/ 的核心逻辑。工具就是"USB 设备",插上就能用。
  2. agent/core.py 独立:对应 Function Calling 核心。这里是处理"模型思考→工具调用→结果回填"循环的地方,是 Agent 的"大脑皮层"。
  3. agent/memory.py 独立:对应 状态管理。对话历史是独立的资源,方便后续替换成数据库或 Redis。
  4. main.py vs web_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(...) 带记忆的对话方法 把记忆内容拼接到用户输入里,让模型能"记得之前说过什么"

为什么要这么写?

  1. 类封装:把 Agent 封装成类,方便多次复用。如果写成函数,状态管理会很混乱。
  2. LangChain 封装create_tool_calling_agent 和 AgentExecutor 是 LangChain 的核心抽象,处理了 Function Calling 的 JSON 解析、错误重试、循环控制等繁琐逻辑。
  3. 配置分离: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() 获取上下文字符串 把记忆格式化成人类可读的文本,拼接到用户输入里

为什么要这么写?

  1. 独立类:记忆逻辑独立出来,方便后续替换。今天用内存列表,明天可以换成 Redis 或数据库。
  2. 长度限制:LLM 有 token 上限,记忆无限增长会导致请求失败。max_messages 是最简单的滑动窗口策略。
  3. 格式化输出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 错误处理 捕获异常 网络请求可能失败,要给用户友好的错误提示,而不是堆栈跟踪

为什么要这么写?

  1. @tool 装饰器:这是 LangChain 的语法糖,自动把函数转换成工具对象,省去手动定义 namedescriptionargs_schema 的麻烦。
  2. 文档字符串要详细:模型靠这个判断"什么时候用这个工具"。写模糊了,模型可能该用时不用,不该用时乱用。
  3. 真实 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 错误捕获 防止程序崩溃,给用户友好提示

为什么要这么写?

  1. 简单直接:命令行是最快的验证方式,不需要额外依赖。
  2. 交互命令clear 命令让用户能主动清空记忆,调试更方便。
  3. 分离关注点:交互逻辑和核心逻辑分离,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?

  1. 快速原型:几行代码就能搭建 Web 界面,适合演示和测试。
  2. 状态管理session_state 天然适合保存对话历史。
  3. 美观:比命令行更友好,适合给非技术人员使用。

如果去掉这部分:
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。这是当前最“标准化”的做法。

步骤概览

  1. 创建一个 Python 项目,定义 MCP Server

  2. 将 get_weather 函数包装成 MCP 的 Tool 资源

  3. 通过 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       # 技能配置

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐