返回AI笔记
AI Day 55

AI Day 55: 实战(5):Agent开发实战 — 构建能用工具的AI助手

AI Day 55: 实战(5):Agent开发实战 — 构建能用工具的AI助手

2026-05-26

日期: 2026-05-26 | 阶段: 第五阶段 · 动手实战 (Day 51-60) | 主题: Agent Development in Practice

学习路径 / Learning Path

AI/LLM 深度技术学习 60天计划
├── 第一阶段:模型基础 (Day 1-15) ✅
│   ├── Day 1: Transformer与LLM基础 ✅
│   ├── Day 2: 量化与本地部署 ✅
│   ├── Day 3: 训练全流程 ✅
│   ├── Day 4: Prompt Engineering ✅
│   ├── Day 5: RAG架构 ✅
│   ├── Day 6: 向量数据库与Embedding ✅
│   ├── Day 7: 微调技术 ✅
│   ├── Day 8: 推理优化 ✅
│   ├── Day 9: 长上下文技术 ✅
│   ├── Day 10: 多模态模型 ✅
│   ├── Day 11: 推理模型 ✅
│   ├── Day 12: Agent框架 ✅
│   ├── Day 13: MCP协议 ✅
│   ├── Day 14: 模型评估 ✅
│   └── Day 15: 阶段一总结 ✅
├── 第二阶段:工程实践 (Day 16-30) ✅
│   ├── Day 16: LLM应用架构 ✅
│   ├── Day 17: 安全与护栏 ✅
│   ├── Day 18: 可观测性 ✅
│   ├── Day 19: 生产RAG·解析与分块 ✅
│   ├── Day 20: 生产RAG·检索与重排 ✅
│   ├── Day 21: 生产RAG·评估与迭代 ✅
│   ├── Day 22: Agent状态与恢复 ✅
│   ├── Day 23: Agent成本优化 ✅
│   ├── Day 24: 多Agent系统 ✅
│   ├── Day 25: Agent测试部署 ✅
│   ├── Day 26: LLM成本工程 ✅
│   ├── Day 27: 多模型编排 ✅
│   ├── Day 28: LLM应用测试 ✅
│   ├── Day 29: 企业LLM平台 ✅
│   └── Day 30: 阶段二总结 ✅
├── 第三阶段:金融零售AI应用 (Day 31-42) ✅
│   ├── Day 31: 金融AI风控 ✅
│   ├── Day 32: 智能投顾与量化 ✅
│   ├── Day 33: 合规与RegTech ✅
│   ├── Day 34: 信贷AI全链路 ✅
│   ├── Day 35: 金融AI总结 ✅
│   ├── Day 36: 零售AI推荐 ✅
│   ├── Day 37: 智能客服 ✅
│   ├── Day 38: 供应链AI ✅
│   ├── Day 39: 智能营销 ✅
│   ├── Day 40: 零售AI总结 ✅
│   ├── Day 41: CeFi×DeFi×AI融合 ✅
│   └── Day 42: AI融合案例与职业 ✅
├── 第四阶段:面试冲刺 (Day 43-50) ✅
│   ├── Day 43: 系统设计·LLM平台 ✅
│   ├── Day 44: 系统设计·RAG系统 ✅
│   ├── Day 45: 系统设计·Agent系统 ✅
│   ├── Day 46: 系统设计·推荐系统 ✅
│   ├── Day 47: 面试·产品AI ✅
│   ├── Day 48: 面试·架构AI ✅
│   ├── Day 49: 面试·行为AI ✅
│   └── Day 50: 学习总结 ✅
└── 第五阶段:动手实战 (Day 51-60)
    ├── Day 51: 本地大模型部署全流程 ✅
    ├── Day 52: RAG系统实战:从文档到问答 ✅
    ├── Day 53: RAG进阶:评估优化与生产化 ✅
    ├── Day 54: LoRA微调实战:训练你的专属模型 ✅
    ├── Day 55: Agent开发实战:构建工具调用Agent ← 你在这里
    ├── Day 56: MCP Server开发:扩展AI能力边界
    ├── Day 57: 多模态应用:图文理解与文档分析
    ├── Day 58: AI应用全栈开发:前后端集成
    ├── Day 59: 性能调优与成本实战
    └── Day 60: 总结与作品集

核心概念 / Core Concepts

Agent = LLM + Tools + Memory / 从"回答问题"到"完成任务"

前几天做的事情 vs 今天做的事情:

Day 52-53 RAG:
  用户提问 → 检索文档 → 生成回答
  LLM 的角色:回答者(被动)
  能力边界:只能用检索到的信息

Day 54 微调:
  训练数据 → 优化模型权重 → 更好地回答
  LLM 的角色:知识库(被动)
  能力边界:只能用训练过的知识

Day 55 Agent:
  用户任务 → 思考 → 使用工具 → 观察结果 → 继续思考 → 完成任务
  LLM 的角色:决策者(主动)
  能力边界:工具有什么能力,Agent就有什么能力

Agent 的本质:
  LLM = 大脑(推理和决策)
  Tools = 手和脚(执行动作)
  Memory = 记忆(保持上下文)

  传统程序: if/else → 执行 (确定性)
  Agent:     思考 → 决策 → 执行 → 反思 (自主性)

Day 12 理论 → Day 55 实践 / Theory to Practice

Day 12 学了什么:
├── Agent 的架构模式(ReAct、Plan-and-Execute)
├── Tool Calling 机制
├── 多 Agent 协作
└── 框架对比(LangChain、AutoGPT、CrewAI)

Day 22-25 学了什么:
├── Agent 状态管理与错误恢复
├── Agent 成本优化
├── 多 Agent 通信模式
└── Agent 测试部署

Day 45 设计了什么:
├── Agent 系统架构(面试题)
└── 安全、监控、回退策略

今天要做什么:
  把上面所有理论,变成一个能运行的 Agent!

框架选择:LangGraph
  Day 12 对比过框架,选 LangGraph 因为:
  1. 状态管理最清晰(Graph + State)
  2. 可视化调试友好
  3. LangSmith 集成
  4. 适合复杂 Agent(多步骤、条件分支)

知识点1:LangGraph 入门 / LangGraph Fundamentals

核心概念 / Core Concepts

LangGraph 的四个基本元素:

1. State(状态)
   Agent 在执行过程中的"记忆"
   包含:消息历史、工具调用结果、中间变量
   每一步都会更新 State

2. Node(节点)
   执行具体操作的函数
   例如:调用 LLM、执行工具、处理结果
   每个节点接收 State,返回更新后的 State

3. Edge(边)
   节点之间的连接关系
   决定执行流程:顺序、条件分支、循环

4. Graph(图)
   由 Node + Edge 组成的完整工作流
   定义了 Agent 的整个执行逻辑

类比:
  State = 你脑中的想法
  Node  = 你做的每个动作
  Edge  = 动作之间的逻辑关系
  Graph = 整个任务的执行计划

安装配置 / Installation

# === Agent 开发依赖 ===
pip install langgraph langchain langchain-community langchain-openai
pip install tavily-python     # Web 搜索工具
pip install httpx             # HTTP 请求
pip install python-dotenv     # 环境变量管理

# === 可选:可视化和调试 ===
pip install langsmith         # Trace 和调试
pip install grandalf          # Graph 可视化

第一个 Agent:计算器 Agent / First Agent: Calculator

"""
calculator_agent.py
最简单的 Agent — 能做数学计算

为什么从计算器开始?
  1. 工具逻辑简单,容易调试
  2. 能清楚展示 Agent 的 ReAct 循环
  3. LLM 本身不擅长数学,工具价值明显
"""

from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, SystemMessage
import json
import math

# ==========================================
# 1. 定义状态
# ==========================================

class AgentState(TypedDict):
    """Agent 的状态定义"""
    messages: Annotated[list, add_messages]
    # add_messages 会自动合并新消息到历史中

# ==========================================
# 2. 定义工具
# ==========================================

@tool
def calculator(expression: str) -> str:
    """计算数学表达式。输入一个数学表达式字符串,返回计算结果。
    例如: "2 + 3 * 4" 返回 "14"
    支持: +, -, *, /, **, sqrt, sin, cos, log
    """
    try:
        # 安全的数学运算环境
        allowed_names = {
            "sqrt": math.sqrt,
            "sin": math.sin,
            "cos": math.cos,
            "tan": math.tan,
            "log": math.log,
            "log10": math.log10,
            "pi": math.pi,
            "e": math.e,
            "abs": abs,
            "round": round,
        }
        result = eval(expression, {"__builtins__": {}}, allowed_names)
        return f"计算结果: {expression} = {result}"
    except Exception as e:
        return f"计算错误: {e}"

# 工具列表
tools = [calculator]

# ==========================================
# 3. 创建 LLM (带工具绑定)
# ==========================================

# 使用 Ollama 本地模型
llm = ChatOpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama",
    model="qwen2.5:7b",
    temperature=0,
)

# 绑定工具 — 让 LLM 知道有哪些工具可用
llm_with_tools = llm.bind_tools(tools)

# ==========================================
# 4. 定义节点
# ==========================================

def agent_node(state: AgentState) -> AgentState:
    """Agent 思考节点:调用 LLM 决定下一步"""
    system_msg = SystemMessage(
        content="你是一个数学助手。当需要计算时,使用 calculator 工具。"
    )
    messages = [system_msg] + state["messages"]
    response = llm_with_tools.invoke(messages)
    return {"messages": [response]}

def tool_node(state: AgentState) -> AgentState:
    """工具执行节点:执行 LLM 选择的工具"""
    last_message = state["messages"][-1]
    results = []

    for tool_call in last_message.tool_calls:
        tool_name = tool_call["name"]
        tool_args = tool_call["args"]

        # 找到对应工具并执行
        tool_fn = {t.name: t for t in tools}.get(tool_name)
        if tool_fn:
            result = tool_fn.invoke(tool_args)
            results.append({
                "role": "tool",
                "content": str(result),
                "tool_call_id": tool_call["id"],
            })

    return {"messages": results}

# ==========================================
# 5. 定义边(路由逻辑)
# ==========================================

def should_use_tool(state: AgentState) -> str:
    """决定是否需要调用工具"""
    last_message = state["messages"][-1]
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        return "tools"      # 需要调用工具
    return END              # 直接结束(已有答案)

# ==========================================
# 6. 构建 Graph
# ==========================================

graph = StateGraph(AgentState)

# 添加节点
graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)

# 设置入口
graph.set_entry_point("agent")

# 添加边
graph.add_conditional_edges(
    "agent",
    should_use_tool,
    {
        "tools": "tools",   # 如果需要工具 → 执行工具
        END: END,            # 如果不需要 → 结束
    },
)
graph.add_edge("tools", "agent")  # 工具执行后 → 回到 Agent 思考

# 编译
app = graph.compile()

# ==========================================
# 7. 运行测试
# ==========================================

def run_agent(question: str):
    """运行 Agent 并打印每一步"""
    print(f"\n{'='*60}")
    print(f"Question: {question}")
    print(f"{'='*60}")

    result = app.invoke({
        "messages": [HumanMessage(content=question)]
    })

    # 打印完整的消息链
    for i, msg in enumerate(result["messages"]):
        role = msg.__class__.__name__
        content = msg.content[:200] if msg.content else "[tool_call]"
        print(f"\n  Step {i}: [{role}]")
        print(f"  {content}")

        if hasattr(msg, "tool_calls") and msg.tool_calls:
            for tc in msg.tool_calls:
                print(f"  → Tool: {tc['name']}({tc['args']})")

    return result


if __name__ == "__main__":
    # 简单计算
    run_agent("请计算 (15 + 27) * 3 - 10")

    # 多步计算
    run_agent("一个圆的半径是5,求它的面积和周长")

    # 不需要工具的问题
    run_agent("你好,你是谁?")

知识点2:工具开发 / Tool Development

自定义工具集 / Custom Tool Suite

"""
tools.py
Agent 的工具集 — 每个工具扩展 Agent 的一个能力

Day 12 学过:
  "Agent 的能力 = LLM 的推理能力 × 工具的执行能力"
  "工具设计的好坏直接影响 Agent 的表现"

工具设计原则:
1. 单一职责:每个工具只做一件事
2. 清晰描述:LLM 靠描述决定是否使用
3. 输入验证:防止 LLM 传入非法参数
4. 错误处理:优雅地处理失败
"""

from langchain_core.tools import tool
from typing import Optional
import httpx
import json
from pathlib import Path

# ==========================================
# 工具1: 文件读取
# ==========================================

@tool
def read_file(file_path: str) -> str:
    """读取本地文件内容。
    输入文件的绝对路径或相对路径,返回文件的文本内容。
    支持 .md, .txt, .py, .json 等文本文件。
    如果文件不存在或无法读取,返回错误信息。
    """
    try:
        path = Path(file_path)

        # 安全检查:只允许读取特定目录
        allowed_dirs = ["docs/", "src/", "notes/"]
        is_allowed = any(
            str(path).replace("\\", "/").startswith(d)
            for d in allowed_dirs
        )

        if not is_allowed:
            return f"安全限制:不允许读取 {file_path},只能访问 docs/, src/, notes/ 目录"

        if not path.exists():
            return f"文件不存在: {file_path}"

        content = path.read_text(encoding="utf-8")

        # 截断过长内容
        if len(content) > 3000:
            content = content[:3000] + f"\n\n... (truncated, total {len(content)} chars)"

        return content

    except Exception as e:
        return f"读取文件错误: {e}"


# ==========================================
# 工具2: Web 搜索
# ==========================================

@tool
def web_search(query: str, max_results: int = 3) -> str:
    """搜索互联网获取最新信息。
    输入搜索关键词,返回搜索结果的摘要。
    适用于需要最新数据、新闻、或模型知识库中没有的信息。
    max_results: 返回结果数量,默认3条。
    """
    try:
        # 方案1: 使用 Tavily API (推荐,专为 AI Agent 设计)
        from tavily import TavilyClient
        import os

        api_key = os.getenv("TAVILY_API_KEY")
        if not api_key:
            return "错误: 未配置 TAVILY_API_KEY 环境变量"

        client = TavilyClient(api_key=api_key)
        results = client.search(query=query, max_results=max_results)

        formatted = []
        for i, result in enumerate(results.get("results", []), 1):
            formatted.append(
                f"[{i}] {result['title']}\n"
                f"    URL: {result['url']}\n"
                f"    摘要: {result['content'][:200]}"
            )

        return "\n\n".join(formatted) if formatted else "未找到相关结果"

    except ImportError:
        # 方案2: 简化版 — 直接告诉 Agent 搜索不可用
        return (
            f"Web搜索暂不可用(未安装 tavily-python)。"
            f"请用其他方式获取关于「{query}」的信息。"
        )
    except Exception as e:
        return f"搜索错误: {e}"


# ==========================================
# 工具3: 数据库查询 (SQLite)
# ==========================================

@tool
def query_database(sql: str, db_path: str = "data/notes.db") -> str:
    """执行 SQL 查询获取结构化数据。
    输入 SQL 语句(仅支持 SELECT),返回查询结果。
    数据库包含学习笔记的元数据。
    表结构: notes(id, title, day, category, tags, created_at)
    """
    import sqlite3

    # 安全检查:只允许 SELECT
    sql_upper = sql.strip().upper()
    if not sql_upper.startswith("SELECT"):
        return "安全限制: 只允许 SELECT 查询"

    dangerous_keywords = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER", "CREATE"]
    if any(kw in sql_upper for kw in dangerous_keywords):
        return f"安全限制: 不允许修改数据库的操作"

    try:
        conn = sqlite3.connect(db_path)
        cursor = conn.execute(sql)
        columns = [desc[0] for desc in cursor.description]
        rows = cursor.fetchall()
        conn.close()

        if not rows:
            return "查询结果为空"

        # 格式化输出
        result = f"列: {', '.join(columns)}\n"
        result += f"行数: {len(rows)}\n\n"
        for row in rows[:20]:  # 最多返回20行
            result += " | ".join(str(v) for v in row) + "\n"

        if len(rows) > 20:
            result += f"\n... (共 {len(rows)} 行,显示前 20 行)"

        return result

    except Exception as e:
        return f"查询错误: {e}"


# ==========================================
# 工具4: API 调用 (通用 HTTP)
# ==========================================

@tool
def call_api(url: str, method: str = "GET", params: Optional[str] = None) -> str:
    """调用外部 API 获取数据。
    url: API 的完整 URL
    method: HTTP 方法,GET 或 POST
    params: JSON 格式的参数字符串(可选)
    返回 API 响应内容。
    """
    try:
        # URL 白名单 (安全考虑)
        allowed_domains = [
            "api.coingecko.com",
            "api.llama.fi",        # DeFiLlama
            "api.etherscan.io",
        ]

        from urllib.parse import urlparse
        domain = urlparse(url).netloc
        if domain not in allowed_domains:
            return f"安全限制: 只允许访问 {', '.join(allowed_domains)}"

        headers = {"Accept": "application/json"}

        if method.upper() == "GET":
            response = httpx.get(url, headers=headers, timeout=10)
        elif method.upper() == "POST":
            body = json.loads(params) if params else {}
            response = httpx.post(url, json=body, headers=headers, timeout=10)
        else:
            return f"不支持的 HTTP 方法: {method}"

        response.raise_for_status()

        # 截断过长响应
        content = response.text
        if len(content) > 3000:
            content = content[:3000] + "... (truncated)"

        return content

    except httpx.TimeoutException:
        return f"API 请求超时: {url}"
    except httpx.HTTPStatusError as e:
        return f"API 返回错误: {e.response.status_code}"
    except Exception as e:
        return f"API 调用错误: {e}"


# ==========================================
# 工具注册表
# ==========================================

ALL_TOOLS = [
    calculator,     # 从知识点1导入
    read_file,
    web_search,
    query_database,
    call_api,
]

def get_tools(names: list[str] = None) -> list:
    """获取工具列表,可按名称过滤"""
    if names is None:
        return ALL_TOOLS
    return [t for t in ALL_TOOLS if t.name in names]

工具装饰器详解 / Tool Decorator Deep Dive

@tool 装饰器做了什么?

1. 自动从 docstring 生成工具描述
   LLM 用这个描述决定是否调用工具

2. 自动从类型注解生成参数 schema
   LLM 知道应该传什么参数

3. 包装成标准的 Tool 对象
   框架能统一管理和调用

关键原则:docstring 要写给 LLM 看

好的描述:
  "搜索互联网获取最新信息。
   输入搜索关键词,返回搜索结果的摘要。
   适用于需要最新数据、新闻、或模型知识库中没有的信息。"

差的描述:
  "Web search function."
  → LLM 不知道什么时候该用它

LLM 的决策过程:
  1. 看到用户问题
  2. 看所有工具的描述
  3. 选择最匹配的工具
  4. 根据参数 schema 构造调用参数

所以:工具描述 = 给 LLM 的使用说明书

知识点3:ReAct Agent 实现 / ReAct Agent Implementation

完整的 ReAct 循环 / Full ReAct Loop

"""
react_agent.py
完整的 ReAct Agent — Thought → Action → Observation → Answer

Day 12 学的 ReAct 模式:
  Reasoning + Acting 交替进行
  让 LLM 先"想"再"做",做完"看"结果,再继续"想"

今天实现完整版本,支持:
  - 多轮工具调用
  - 错误重试
  - 最大步数限制(防无限循环)
  - 详细日志
"""

from typing import Annotated, TypedDict, Literal
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langchain_openai import ChatOpenAI
from langchain_core.messages import (
    HumanMessage, SystemMessage, AIMessage, ToolMessage,
)
import time

# ==========================================
# 1. 增强的 State 定义
# ==========================================

class ReActState(TypedDict):
    """ReAct Agent 状态"""
    messages: Annotated[list, add_messages]
    step_count: int        # 当前步数
    max_steps: int         # 最大步数限制

# ==========================================
# 2. 系统提示词
# ==========================================

REACT_SYSTEM_PROMPT = """你是一个智能助手,能够使用工具来完成任务。

## 工作方式
1. 先分析用户的请求,理解需要做什么
2. 如果需要外部信息或计算,使用合适的工具
3. 根据工具返回的结果,组织回答
4. 如果一次工具调用不够,可以连续使用多个工具

## 注意事项
- 优先使用工具获取准确数据,不要猜测
- 如果工具调用失败,尝试换个方式
- 每一步都要解释你的思考过程
- 最终回答要全面、有条理

## 可用工具
你可以使用以下工具(具体描述见工具定义):
- calculator: 数学计算
- read_file: 读取本地文件
- web_search: 搜索互联网
- query_database: 查询数据库
- call_api: 调用外部API
"""

# ==========================================
# 3. Agent 节点
# ==========================================

from tools import ALL_TOOLS

llm = ChatOpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama",
    model="qwen2.5:7b",
    temperature=0,
)

llm_with_tools = llm.bind_tools(ALL_TOOLS)

def agent_node(state: ReActState) -> dict:
    """Agent 思考和决策节点"""
    step = state.get("step_count", 0)
    max_steps = state.get("max_steps", 10)

    # 步数限制保护
    if step >= max_steps:
        return {
            "messages": [
                AIMessage(content=f"已达到最大步数限制({max_steps}),基于已有信息给出回答。")
            ],
            "step_count": step + 1,
        }

    # 构造消息
    system_msg = SystemMessage(content=REACT_SYSTEM_PROMPT)
    messages = [system_msg] + state["messages"]

    # 调用 LLM
    start = time.time()
    response = llm_with_tools.invoke(messages)
    elapsed = time.time() - start

    # 日志
    print(f"\n  [Step {step + 1}] Agent thinking... ({elapsed:.1f}s)")
    if response.content:
        print(f"  Thought: {response.content[:150]}...")
    if hasattr(response, "tool_calls") and response.tool_calls:
        for tc in response.tool_calls:
            print(f"  Action: {tc['name']}({json.dumps(tc['args'], ensure_ascii=False)[:100]})")

    return {
        "messages": [response],
        "step_count": step + 1,
    }

# ==========================================
# 4. 工具执行节点 (使用 LangGraph 内置)
# ==========================================

tool_node = ToolNode(ALL_TOOLS)

def tool_executor(state: ReActState) -> dict:
    """工具执行节点 — 包装 ToolNode,添加日志"""
    last_message = state["messages"][-1]

    print(f"\n  [Tool Execution]")
    start = time.time()

    # 执行工具
    result = tool_node.invoke(state)

    elapsed = time.time() - start

    # 打印工具结果
    for msg in result.get("messages", []):
        if hasattr(msg, "content"):
            print(f"  Observation: {str(msg.content)[:200]}...")

    print(f"  Tool completed in {elapsed:.1f}s")

    return result

# ==========================================
# 5. 路由逻辑
# ==========================================

import json

def route_agent(state: ReActState) -> Literal["tools", "__end__"]:
    """决定下一步:调用工具还是结束"""
    last_message = state["messages"][-1]
    step = state.get("step_count", 0)
    max_steps = state.get("max_steps", 10)

    # 达到步数限制 → 结束
    if step >= max_steps:
        return END

    # 有工具调用 → 执行工具
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        return "tools"

    # 没有工具调用 → Agent 已给出最终回答
    return END

# ==========================================
# 6. 构建 Graph
# ==========================================

graph = StateGraph(ReActState)

graph.add_node("agent", agent_node)
graph.add_node("tools", tool_executor)

graph.set_entry_point("agent")

graph.add_conditional_edges("agent", route_agent)
graph.add_edge("tools", "agent")  # 工具完成 → 回到 Agent

react_agent = graph.compile()

# ==========================================
# 7. 运行示例
# ==========================================

def run_react(question: str, max_steps: int = 10):
    """运行 ReAct Agent"""
    print(f"\n{'='*60}")
    print(f"Task: {question}")
    print(f"Max Steps: {max_steps}")
    print(f"{'='*60}")

    result = react_agent.invoke({
        "messages": [HumanMessage(content=question)],
        "step_count": 0,
        "max_steps": max_steps,
    })

    # 提取最终回答
    final = result["messages"][-1]
    print(f"\n{'='*60}")
    print(f"Final Answer:")
    print(f"{final.content}")
    print(f"Total Steps: {result['step_count']}")
    print(f"{'='*60}")

    return result


if __name__ == "__main__":
    # 示例1: 需要计算的问题
    run_react("一个圆柱体底面半径3cm,高10cm,求体积和表面积")

    # 示例2: 需要文件读取的问题
    run_react("读取 docs/ai/day7-finetuning-lora-qlora.md 的内容,总结LoRA的核心原理")

    # 示例3: 多工具组合
    run_react("查看Day 12的Agent框架笔记,总结ReAct模式,然后计算如果每个Agent步骤耗时2秒,10步Agent完成一个任务需要多少秒")

实际日志展示 / Real Execution Logs

============================================================
Task: 一个圆柱体底面半径3cm,高10cm,求体积和表面积
Max Steps: 10
============================================================

  [Step 1] Agent thinking... (2.3s)
  Thought: 需要计算圆柱体的体积和表面积。
    体积 = π * r² * h
    表面积 = 2πr(r + h)
    我来用计算器工具分别算。
  Action: calculator({"expression": "3.14159 * 3**2 * 10"})

  [Tool Execution]
  Observation: 计算结果: 3.14159 * 3**2 * 10 = 282.7431
  Tool completed in 0.0s

  [Step 2] Agent thinking... (1.8s)
  Thought: 体积计算完成,约282.74 cm³。现在计算表面积。
  Action: calculator({"expression": "2 * 3.14159 * 3 * (3 + 10)"})

  [Tool Execution]
  Observation: 计算结果: 2 * 3.14159 * 3 * (3 + 10) = 245.0441...
  Tool completed in 0.0s

  [Step 3] Agent thinking... (1.5s)
  Thought: 两个计算都完成了,给出最终答案。

============================================================
Final Answer:
圆柱体的计算结果如下:

**体积** = πr²h = π × 3² × 10 ≈ 282.74 cm³
**表面积** = 2πr(r + h) = 2π × 3 × (3 + 10) ≈ 245.04 cm²

其中 r = 3cm, h = 10cm。
Total Steps: 3
============================================================

Agent 的 ReAct 过程:
  Step 1: Thought(分析问题) → Action(计算体积) → Observation(282.74)
  Step 2: Thought(体积OK) → Action(计算面积) → Observation(245.04)
  Step 3: Thought(整合结果) → Answer(最终回答)

知识点4:记忆系统 / Memory System

短期记忆:对话历史 / Short-term Memory: Conversation History

"""
memory.py
Agent 的记忆系统

Day 22 学过 Agent 状态管理:
  "无状态 Agent 像金鱼,每次对话都从头开始"
  "有记忆的 Agent 像人,能记住上下文"

两种记忆:
  短期记忆 = 当前对话的历史(存在 State 里)
  长期记忆 = 跨对话的知识(存在向量数据库里)
"""

from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage

class ConversationMemory:
    """对话记忆管理器"""

    def __init__(self, max_turns: int = 10):
        self.max_turns = max_turns
        self.history: list = []

    def add_turn(self, user_msg: str, agent_msg: str):
        """添加一轮对话"""
        self.history.append({
            "user": user_msg,
            "agent": agent_msg,
        })

        # 裁剪过长的历史
        if len(self.history) > self.max_turns:
            self.history = self.history[-self.max_turns:]

    def get_messages(self) -> list:
        """获取消息列表格式的历史"""
        messages = []
        for turn in self.history:
            messages.append(HumanMessage(content=turn["user"]))
            messages.append(AIMessage(content=turn["agent"]))
        return messages

    def get_summary(self) -> str:
        """获取对话摘要(当历史太长时)"""
        if len(self.history) <= 3:
            return ""

        summary_parts = []
        for turn in self.history[:-3]:  # 总结较早的对话
            summary_parts.append(f"- 用户问: {turn['user'][:50]}...")
            summary_parts.append(f"  助手答: {turn['agent'][:50]}...")

        return "之前的对话摘要:\n" + "\n".join(summary_parts)

    def clear(self):
        """清空记忆"""
        self.history = []

长期记忆:向量存储 / Long-term Memory: Vector Store

"""
long_term_memory.py
长期记忆 — 跨对话的知识存储

结合 Day 52 的 ChromaDB 向量数据库
Agent 可以"回忆"之前对话中的重要信息
"""

import chromadb
from datetime import datetime
import json

class LongTermMemory:
    """基于向量数据库的长期记忆"""

    def __init__(self, persist_dir: str = "./agent_memory"):
        self.client = chromadb.PersistentClient(path=persist_dir)
        self.collection = self.client.get_or_create_collection(
            name="agent_memories",
            metadata={"hnsw:space": "cosine"},
        )

    def store(self, content: str, metadata: dict = None):
        """存储一条记忆"""
        memory_id = f"mem_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}"
        meta = metadata or {}
        meta["timestamp"] = datetime.now().isoformat()

        self.collection.add(
            documents=[content],
            metadatas=[meta],
            ids=[memory_id],
        )

    def recall(self, query: str, n_results: int = 3) -> list[str]:
        """回忆与查询相关的记忆"""
        results = self.collection.query(
            query_texts=[query],
            n_results=n_results,
        )

        memories = []
        for doc, meta in zip(
            results["documents"][0],
            results["metadatas"][0],
        ):
            timestamp = meta.get("timestamp", "unknown")
            memories.append(f"[{timestamp}] {doc}")

        return memories

    def store_conversation_summary(self, user_msg: str, agent_msg: str):
        """存储对话摘要作为长期记忆"""
        summary = f"用户问: {user_msg}\n助手答: {agent_msg[:200]}"
        self.store(
            content=summary,
            metadata={
                "type": "conversation",
                "user_query": user_msg[:100],
            },
        )

    def get_relevant_context(self, question: str) -> str:
        """获取与当前问题相关的历史记忆"""
        memories = self.recall(question, n_results=3)
        if not memories:
            return ""

        return (
            "## 相关历史记忆\n"
            + "\n".join(f"- {m}" for m in memories)
        )

记忆检索与注入 / Memory Retrieval & Injection

"""
memory_agent.py
带记忆的 Agent — 整合短期和长期记忆

Agent 在每次思考前:
1. 从短期记忆获取对话上下文
2. 从长期记忆检索相关知识
3. 将两者注入到 System Prompt 中
"""

class MemoryAgent:
    """带完整记忆系统的 Agent"""

    def __init__(self):
        self.short_memory = ConversationMemory(max_turns=10)
        self.long_memory = LongTermMemory()

    def chat(self, user_input: str) -> str:
        """带记忆的对话"""

        # 1. 检索长期记忆
        relevant_memories = self.long_memory.get_relevant_context(user_input)

        # 2. 构建增强的 System Prompt
        enhanced_prompt = REACT_SYSTEM_PROMPT
        if relevant_memories:
            enhanced_prompt += f"\n\n{relevant_memories}"

        # 3. 获取对话历史
        history_messages = self.short_memory.get_messages()

        # 4. 运行 Agent
        messages = (
            [SystemMessage(content=enhanced_prompt)]
            + history_messages
            + [HumanMessage(content=user_input)]
        )

        result = react_agent.invoke({
            "messages": messages,
            "step_count": 0,
            "max_steps": 10,
        })

        # 5. 提取回答
        answer = result["messages"][-1].content

        # 6. 更新记忆
        self.short_memory.add_turn(user_input, answer)
        self.long_memory.store_conversation_summary(user_input, answer)

        return answer


# 使用示例
agent = MemoryAgent()

# 第一轮对话
agent.chat("LoRA的rank参数一般设置多少?")

# 第二轮对话(Agent 能记住上下文)
agent.chat("那alpha呢?")  # Agent 知道你在问 LoRA 的 alpha

# 下次启动(长期记忆仍在)
agent2 = MemoryAgent()
agent2.chat("我们之前讨论过LoRA参数,能回忆一下吗?")
# → Agent 从长期记忆中检索到之前的对话

知识点5:实战项目 — Web3 研究 Agent / Web3 Research Agent

项目架构 / Project Architecture

Web3 Research Agent 功能:
  输入: 协议名称(如 "Uniswap")
  过程: 自动收集数据 → 分析 → 生成报告
  输出: 结构化的协议分析报告

数据来源:
├── CoinGecko API: 价格、市值、交易量
├── DeFiLlama API: TVL、协议排名
├── Web 搜索: 最新新闻和动态
└── 本地笔记: 已有的分析和知识

Agent 工作流:
  1. 接收协议名称
  2. 查询 CoinGecko 获取基本数据
  3. 查询 DeFiLlama 获取 TVL 数据
  4. 搜索最新新闻
  5. 综合所有信息生成分析报告

完整代码 / Full Implementation

"""
web3_research_agent.py
Web3 协议研究 Agent

结合了:
  Day 12 的 Agent 架构
  Day 34 的 Web3 数据指标
  Day 52 的 RAG 系统
  → 自动化的协议研究
"""

from langchain_core.tools import tool
import httpx
import json

# ==========================================
# Web3 专用工具
# ==========================================

@tool
def get_token_price(token_id: str) -> str:
    """获取加密货币的当前价格和市场数据。
    token_id: CoinGecko 的代币ID,如 'ethereum', 'uniswap', 'aave'
    返回: 价格、24h变化、市值、24h交易量等数据
    """
    try:
        url = f"https://api.coingecko.com/api/v3/coins/{token_id}"
        params = {
            "localization": "false",
            "tickers": "false",
            "community_data": "false",
            "developer_data": "false",
        }

        response = httpx.get(url, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()

        market = data.get("market_data", {})
        result = {
            "name": data.get("name"),
            "symbol": data.get("symbol", "").upper(),
            "price_usd": market.get("current_price", {}).get("usd"),
            "price_change_24h": market.get("price_change_percentage_24h"),
            "market_cap_usd": market.get("market_cap", {}).get("usd"),
            "volume_24h_usd": market.get("total_volume", {}).get("usd"),
            "ath_usd": market.get("ath", {}).get("usd"),
            "ath_change_pct": market.get("ath_change_percentage", {}).get("usd"),
        }

        return json.dumps(result, indent=2, ensure_ascii=False)

    except httpx.HTTPStatusError as e:
        return f"API错误: {e.response.status_code}. 请检查 token_id 是否正确。"
    except Exception as e:
        return f"获取价格数据失败: {e}"


@tool
def get_protocol_tvl(protocol_slug: str) -> str:
    """获取 DeFi 协议的 TVL(总锁仓量)数据。
    protocol_slug: DeFiLlama 的协议名称,如 'uniswap', 'aave', 'lido'
    返回: 当前TVL、TVL变化、链分布等数据
    """
    try:
        url = f"https://api.llama.fi/protocol/{protocol_slug}"
        response = httpx.get(url, timeout=10)
        response.raise_for_status()
        data = response.json()

        # 提取关键信息
        current_tvl = data.get("currentChainTvls", {})
        total_tvl = sum(
            v for k, v in current_tvl.items()
            if not k.endswith("-staking") and not k.endswith("-borrowed")
        )

        result = {
            "name": data.get("name"),
            "category": data.get("category"),
            "total_tvl_usd": total_tvl,
            "chains": list(current_tvl.keys())[:10],
            "chain_tvls": {
                k: v for k, v in sorted(
                    current_tvl.items(), key=lambda x: x[1], reverse=True
                )[:5]
            },
            "description": data.get("description", "")[:200],
        }

        return json.dumps(result, indent=2, ensure_ascii=False)

    except Exception as e:
        return f"获取TVL数据失败: {e}"


@tool
def search_web3_news(query: str) -> str:
    """搜索 Web3/加密货币相关的最新新闻和动态。
    query: 搜索关键词,如 'Uniswap v4 latest news'
    返回最新的相关新闻摘要。
    """
    # 复用 web_search,添加 Web3 上下文
    enhanced_query = f"{query} crypto DeFi 2026"
    return web_search.invoke({"query": enhanced_query, "max_results": 3})


# ==========================================
# 研究 Agent
# ==========================================

WEB3_RESEARCH_PROMPT = """你是一个专业的 Web3 协议研究分析师。

## 任务
收集并分析指定协议的数据,生成结构化的研究报告。

## 研究流程
1. 先用 get_token_price 获取代币价格和市场数据
2. 再用 get_protocol_tvl 获取 TVL 数据
3. 用 search_web3_news 搜索最新动态
4. 综合所有数据,生成分析报告

## 报告格式
请按以下格式输出研究报告:

### 协议概览
- 名称、类别、核心功能

### 市场数据
- 代币价格、市值、交易量、ATH对比

### TVL分析
- 当前TVL、链分布、趋势判断

### 最新动态
- 近期重要新闻和事件

### 综合评估
- 优势、风险、PM视角的产品洞察

注意:所有数据必须来自工具调用结果,不要编造数据。
"""

# 工具列表
web3_tools = [get_token_price, get_protocol_tvl, search_web3_news]

# 构建 Agent (复用 ReAct 架构)
from langchain_openai import ChatOpenAI

research_llm = ChatOpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama",
    model="qwen2.5:7b",
    temperature=0.3,
)

research_llm_with_tools = research_llm.bind_tools(web3_tools)

# ... (使用与知识点3相同的 Graph 架构,替换工具和 Prompt)

def research_protocol(protocol_name: str) -> str:
    """运行协议研究"""
    question = f"请对 {protocol_name} 协议进行全面研究分析。"
    result = run_react(question, max_steps=8)
    return result["messages"][-1].content


# 运行示例
if __name__ == "__main__":
    report = research_protocol("Uniswap")
    print(report)

    # 保存报告
    with open(f"research_uniswap.md", "w", encoding="utf-8") as f:
        f.write(report)

输出示例 / Sample Output

# Uniswap 协议研究报告

## 协议概览
- **名称**: Uniswap
- **类别**: DEX (去中心化交易所)
- **核心功能**: 基于 AMM 的代币交换、流动性提供

## 市场数据
- **代币**: UNI
- **当前价格**: $12.34
- **24h 变化**: +3.2%
- **市值**: $74.04 亿
- **24h 交易量**: $2.89 亿
- **ATH**: $44.97 (当前距ATH -72.6%)

## TVL 分析
- **总 TVL**: $48.2 亿
- **链分布**:
  - Ethereum: $32.1 亿 (66.6%)
  - Arbitrum: $8.5 亿 (17.6%)
  - Polygon: $3.2 亿 (6.6%)
  - Base: $2.8 亿 (5.8%)

## 最新动态
- Uniswap V4 hooks 生态持续扩展
- ...

## 综合评估
- **优势**: DEX 市场份额领先,多链部署完善
- **风险**: 监管不确定性,竞争加剧
- **PM 洞察**: V4 的 hooks 架构开启了第三方创新...

知识点6:Agent 调试 / Agent Debugging

LangSmith Trace 查看 / LangSmith Tracing

"""
debug_setup.py
Agent 调试配置

Day 18 学过可观测性:
  "没有 Trace 的 Agent = 黑盒"
  "出了问题不知道哪一步出错"

LangSmith 提供:
  - 完整的执行链路
  - 每一步的输入输出
  - Token 消耗统计
  - 延迟分析
"""

import os

# 配置 LangSmith (免费版足够)
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "your-langsmith-api-key"
os.environ["LANGCHAIN_PROJECT"] = "web3-research-agent"

# 配置后,所有 LangChain/LangGraph 调用都会自动上报 Trace
# 在 https://smith.langchain.com 可以查看

"""
Trace 中你能看到:

Run: research_protocol("Uniswap")
├── agent_node (Step 1)
│   ├── Input: [SystemMessage, HumanMessage]
│   ├── LLM Call: 2.3s, 450 tokens
│   └── Output: AIMessage(tool_calls=[get_token_price])
│
├── tool_executor (Step 1)
│   ├── Tool: get_token_price("uniswap")
│   ├── HTTP Request: api.coingecko.com (0.8s)
│   └── Output: ToolMessage(price data)
│
├── agent_node (Step 2)
│   ├── Input: [..., ToolMessage]
│   ├── LLM Call: 1.9s, 380 tokens
│   └── Output: AIMessage(tool_calls=[get_protocol_tvl])
│
├── tool_executor (Step 2)
│   ├── Tool: get_protocol_tvl("uniswap")
│   ├── HTTP Request: api.llama.fi (1.2s)
│   └── Output: ToolMessage(TVL data)
│
└── agent_node (Step 3)
    ├── Input: [..., ToolMessage, ToolMessage]
    ├── LLM Call: 3.1s, 820 tokens
    └── Output: AIMessage(final report)

Total: 3 steps, 9.3s, 1650 tokens
"""

常见问题与解决方案 / Common Issues & Solutions

问题1: 工具调用失败
─────────────────────────────
症状: Agent 调用工具后收到错误信息
原因: API 超时、参数错误、网络问题

解决方案:
  1. 在工具中添加重试逻辑
  2. 返回友好的错误信息(让 Agent 能理解并换个方式)
  3. 设置合理的超时时间

  # 工具中的重试模式
  @tool
  def robust_api_call(url: str) -> str:
      """带重试的 API 调用"""
      for attempt in range(3):
          try:
              response = httpx.get(url, timeout=10)
              return response.text
          except httpx.TimeoutException:
              if attempt < 2:
                  continue
              return "API 超时,请稍后重试或换个数据源"


问题2: 无限循环
─────────────────────────────
症状: Agent 反复调用同一个工具,不给出最终答案
原因: LLM 不知道何时停止、工具返回不够明确

解决方案:
  1. max_steps 限制(已实现)
  2. 在 System Prompt 中明确说"收集够数据后直接给出答案"
  3. 检测重复工具调用并强制结束

  def route_agent(state):
      # 检测重复调用
      recent_tools = []
      for msg in state["messages"][-6:]:
          if hasattr(msg, "tool_calls") and msg.tool_calls:
              recent_tools.extend(tc["name"] for tc in msg.tool_calls)

      # 连续3次调用同一工具 → 强制结束
      if len(recent_tools) >= 3 and len(set(recent_tools[-3:])) == 1:
          return END

      # 正常路由逻辑...


问题3: 偏离主题
─────────────────────────────
症状: Agent 开始做与任务无关的事情
原因: 工具返回了干扰信息、System Prompt 不够明确

解决方案:
  1. 在 System Prompt 中明确任务边界
  2. 工具返回结果时过滤无关内容
  3. 添加"任务完成检查"节点

  # 添加任务完成检查
  def check_completion(state):
      """检查 Agent 是否在正确轨道上"""
      original_task = state["messages"][0].content
      latest_thought = state["messages"][-1].content

      # 简单的相关性检查
      # 生产环境可以用 LLM 判断
      return "continue"  # or "redirect"


问题4: 工具选择错误
─────────────────────────────
症状: Agent 用错了工具(该搜索的时候去算数)
原因: 工具描述不够清晰

解决方案:
  1. 改进工具的 docstring 描述
  2. 在描述中加入"使用场景"和"不适用场景"
  3. 添加"什么时候用这个工具"的示例

  @tool
  def web_search(query: str) -> str:
      """搜索互联网获取最新信息。

      适用场景:
      - 需要最新新闻和动态
      - 需要实时数据(价格、事件)
      - 模型知识库中没有的信息

      不适用场景:
      - 数学计算(用 calculator)
      - 读取本地文件(用 read_file)
      """

今日思考 / Today's Reflections

思考1:Agent 的"智能"来自工具,不是 LLM / Agent's Intelligence Comes from Tools

一个反直觉的发现:

没有工具的 LLM (Day 52-54):
  "请查一下 ETH 现在的价格"
  → "很抱歉,我的数据截止到2024年..."
  → 智能感: ★★

有工具的 Agent (今天):
  "请查一下 ETH 现在的价格"
  → 调用 get_token_price("ethereum")
  → "ETH 当前价格 $3,456.78,24h +2.3%"
  → 智能感: ★★★★★

同样的 LLM,差异在于:
  Agent 能"动手做事",而不仅仅是"说话"

PM 视角的洞察:
  用户不在乎你用了什么模型
  用户在乎你能帮他做什么
  Agent 的产品价值 = 工具的实用价值 × LLM 的调度能力

所以工具设计是 Agent 产品最重要的工作!

思考2:错误处理是 Agent 的生命线 / Error Handling is Agent's Lifeline

今天调试中遇到最多的问题不是 LLM 不聪明
而是工具出错后 Agent 不知道怎么办

典型场景:
  1. CoinGecko API 超时 → Agent 陷入循环重试
  2. 工具返回空数据 → Agent 开始编造数据
  3. 参数格式错误 → Agent 卡在同一步

Day 22 学的错误恢复策略今天全用上了:
  - 重试 + 指数退避
  - 回退到更简单的方案
  - 向用户报告无法完成

核心教训:
  "Agent 80% 的代码在处理异常情况"
  这和传统软件开发一模一样
  Happy Path 容易,Edge Case 才见真功夫

思考3:Agent 是 PM 的新产品形态 / Agents are a New Product Paradigm

传统产品:
  用户操作 UI → 系统处理 → 返回结果
  PM 设计:页面流程、交互逻辑、数据展示

Agent 产品:
  用户说需求 → Agent 规划 → 使用工具 → 返回结果
  PM 设计:工具能力、Agent 人设、安全边界、成本控制

Agent PM 需要思考的新问题:
  1. 工具选择:给 Agent 什么工具?
     工具太多 → Agent 选择困难
     工具太少 → Agent 能力有限

  2. 安全边界:Agent 能做什么?不能做什么?
     Day 17 学的 Guard Rails 在 Agent 场景更关键

  3. 成本控制:每个请求调多少次工具?
     Day 23 的 Agent 成本优化不是学术话题

  4. 用户信任:Agent 犯错怎么办?
     不像传统产品有确定性的结果
     需要透明度(展示思考过程)和可回退性

这是一个全新的产品设计范式
传统 PM 经验有用但不够
需要理解 AI 的能力和限制

学习资源 / Resources

LangGraph

Agent 开发

调试工具

Web3 API


明日预告 / Tomorrow's Preview

Day 56: MCP Server 开发 — 扩展AI的能力边界

从 Agent 到 MCP:
  今天: 工具直接写在 Agent 代码里
  明天: 工具封装成 MCP Server,任何 AI 客户端都能调用

MCP 的价值:
  Agent 的工具 = 只有你的 Agent 能用
  MCP Server = 任何 AI 应用都能用

  今天写的 Web3 工具 → 明天封装成 MCP Server
  → Claude Code 能直接调用
  → Cursor 能直接调用
  → 任何 MCP 兼容的客户端都能调用

Day 13 学的 MCP 协议理论 → Day 56 的实际开发

明天将实现:
1. MCP SDK 搭建
2. 笔记搜索 MCP Server
3. Web3 数据查询 MCP Server
4. 与 Claude Code 集成测试
5. npm 发布流程

一次开发,处处可用!

Day 55 完成! 从计算器 Agent 到 Web3 研究 Agent,亲手构建了能使用工具的 AI 助手。 Agent = LLM + Tools + Memory,三者缺一不可。 明天用 MCP 把这些工具开放给整个 AI 生态!