deepagents学习打卡day08

个人主页:https://github.com/zbhgis/

打卡记录

项目演示

1.工具调用能力

2.skill调用

3.任务规划

4.工作区文件保存

其他能力具体可自行启动项目运行体验

langsmith记录

1.工具调用能力

2.skill调用

3.任务规划

4.工作区文件保存

完整代码

仓库:https://github.com/zbhgis/simple_agent_demo

model.py

python 复制代码
# -*- coding: utf-8 -*-
"""
模型配置:从环境变量构建 OpenAI 兼容模型客户端
================================================

支持硅基流动 / DeepSeek / OpenAI 等任意 OpenAI 兼容服务。
依赖的环境变量(见 .env.example):
    LLM_API_KEY    必填,你的模型服务 API Key
    LLM_BASE_URL   可选,默认硅基流动
    LLM_MODEL      可选,默认 Qwen/Qwen2.5-72B-Instruct
"""

import os

# 可选:加载 .env 文件(没装 python-dotenv 也不影响,改为手动读环境变量)
try:
    from dotenv import load_dotenv

    load_dotenv()
except ImportError:
    pass

from langchain_openai import ChatOpenAI

DEFAULT_BASE_URL = "https://api.siliconflow.cn/v1"
# 注意:模型 ID 必须与服务商模型列表完全一致,否则服务端报 404 model is not found。
# 硅基流动会持续下架旧模型,可用模型以 https://siliconflow.cn/models 为准。
DEFAULT_MODEL = "Qwen/Qwen3.6-27B"


def build_model() -> ChatOpenAI:
    """从环境变量构建模型客户端,缺省指向硅基流动。"""
    api_key = os.environ.get("LLM_API_KEY", "")
    base_url = os.environ.get("LLM_BASE_URL", DEFAULT_BASE_URL)
    model = os.environ.get("LLM_MODEL", DEFAULT_MODEL)

    if not api_key:
        raise RuntimeError(
            "未配置 LLM_API_KEY。请在 .env 文件或环境变量中设置后重试。"
        )

    return ChatOpenAI(
        model=model,
        api_key=api_key,
        base_url=base_url,
        temperature=0,
        timeout=120,
    )

runtime.py

python 复制代码
# -*- coding: utf-8 -*-
"""
运行时基础组件:文件系统后端 + 控制台输出工具
=============================================

- backend:虚拟文件系统,把 Agent 的文件操作限定在项目目录内(安全)。
  注意:Skill 文件必须位于 backend 根目录之内,Agent 才能读取。
- print_result / print_todos:多轮对话的执行过程打印(工具调用 + 最终回复 + 清单进度)
- chat_loop:通用多轮对话循环(配置了 Checkpointer 的 Agent 均可复用)
"""

import importlib
import os
import sys

# Windows 控制台中文输出保护(所有入口脚本都从这里走一遍)
if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8")
    sys.stderr.reconfigure(encoding="utf-8")


# ----------------------------------------------------------------------
# 跨版本兼容导入:不同 deepagents / langgraph 版本的导出位置不同,
# 按候选顺序探测,全部失败时给出明确指引
# ----------------------------------------------------------------------
def _import_first(candidates: list, name: str):
    for mod_name in candidates:
        try:
            module = importlib.import_module(mod_name)
        except ImportError:
            continue
        if hasattr(module, name):
            return getattr(module, name)
    raise ImportError(
        f"在以下模块中均未找到 {name}: {candidates}。"
        f"请确认已安装 deepagents>=0.7 及其依赖(pip install -r requirements.txt),"
        f"并查看 README「常见问题」中的版本差异说明。"
    )


# 任务规划中间件(第 4 章):0.7 官方文档位于 langchain 侧,
# 部分版本也从 deepagents.middleware / deepagents 根导出
TodoListMiddleware = _import_first(
    ["langchain.agents.middleware", "deepagents.middleware", "deepagents"],
    "TodoListMiddleware",
)

# 文件系统后端:多数版本在 deepagents.backends.filesystem,
# 旧版在包根或 middleware 中
FilesystemBackend = _import_first(
    ["deepagents.backends.filesystem", "deepagents.backends", "deepagents.middleware"],
    "FilesystemBackend",
)

# 检查点(第 4 章):新旧 langgraph 均可命中
InMemorySaver = _import_first(
    ["langgraph.checkpoint.memory", "langgraph.checkpoint"],
    "InMemorySaver",
)

# ----------------------------------------------------------------------
# 虚拟文件系统后端:项目根目录,越权路径会被拦截
# ----------------------------------------------------------------------
ROOT = os.path.dirname(os.path.abspath(__file__))
backend = FilesystemBackend(root_dir=ROOT, virtual_mode=True)

# Skill 目录(相对 backend 根目录,必须使用正斜杠)
SKILLS_DIR = "skills/"


def print_result(result: dict) -> None:
    """打印本轮 Agent 的执行过程(工具调用 + 最终回复)。"""
    for message in result["messages"]:
        msg_type = getattr(message, "type", "")
        if msg_type == "ai":
            tool_calls = getattr(message, "tool_calls", None) or []
            for call in tool_calls:
                print(f"  [调用工具] {call['name']} 参数: {call['args']}")
            content = getattr(message, "content", "")
            if content:
                print(f"\n助手: {content}\n")
        elif msg_type == "tool":
            content = getattr(message, "content", "")
            print(f"  [工具结果] {getattr(message, 'name', '')}: {content}")


def print_todos(result: dict) -> None:
    """打印本轮的任务清单进度(第 4 章:todos 与产物一起核对)。"""
    todos = result.get("todos") or []
    if not todos:
        return
    icons = {"pending": "[ ]", "in_progress": "[~]", "completed": "[x]"}
    print("  [任务清单]")
    for item in todos:
        icon = icons.get(item.get("status"), "[?]")
        print(f"    {icon} {item.get('content', '')}")


def chat_loop(agent, thread_id: str, banner: str) -> None:
    """通用多轮对话循环:每轮只传新输入,状态由 Checkpointer 按 thread_id 接续。"""
    print("=" * 60)
    print(banner)
    print("=" * 60)

    while True:
        try:
            user_input = input("\n你: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\n再见!")
            break

        if not user_input:
            continue
        if user_input.lower() in {"exit", "quit", "q"}:
            print("再见!")
            break

        try:
            result = agent.invoke(
                {"messages": [("user", user_input)]},
                config={"configurable": {"thread_id": thread_id}},
            )
        except Exception as exc:  # noqa: BLE001
            print(f"\n[执行出错] {exc}")
            continue

        print_result(result)
        print_todos(result)

subagents.py

python 复制代码
# -*- coding: utf-8 -*-
"""
子 Agent 定义(第 5 章:字典方式)
==================================

两个简单子 Agent,主 Agent 作为"协调者"按 description 路由委派:

    1. data-collector  数据收集员:查天气/时间,独立上下文里多次调用工具,
                       只把精简结果交还主 Agent
    2. calc-analyzer   计算分析员:calculator 做数值计算,
                       中间计算过程不进入主 Agent 上下文

对应教程:
    https://datawhalechina.github.io/deepagents-in-action/chapters/ch05-subagents/

要点(第 5 章最佳实践):
    - description 要具体、行为导向,主 Agent 靠它决定路由给谁
    - system_prompt 要写清输出格式和字数限制,避免上下文膨胀
    - tools 显式指定后"完全替换"主 Agent 工具集(最小权限原则)
    - 子 Agent 在独立上下文运行,主 Agent 只收到最终结果
"""

from tools import calculator, get_current_time, get_weather, get_weather_online

SUBAGENTS = [
    {
        # 数据收集员:只会"查",不会"算",也不会写文件
        "name": "data-collector",
        "description": (
            "需要查询多个城市的天气、当前时间等外部信息并汇总时使用。"
            "适合一次查询多个对象的场景,例如同时查几个城市的天气。"
            "需要真实实时天气时明确指定使用 get_weather_online 工具。"
        ),
        "system_prompt": (
            "你是数据收集专家。根据要求调用查询工具收集信息,"
            "并为每条数据标注来源(哪个工具、什么参数)。\n"
            "用户没有特别说明时,天气查询优先使用 get_weather_online 获取真实数据;"
            "它返回未配置 Key 或查询失败时,如实标注并回退到 get_weather 的模拟数据,"
            "且必须注明【模拟数据】。\n"
            "输出要求:只返回结构化的数据摘要(每条一行),"
            "控制在 200 字以内;不要给出分析结论,那是别人的工作。\n"
            "如果某个查询失败或工具报错,在结果中如实标注【查询失败】及原因,"
            "绝不允许编造或猜测数据。"
        ),
        "tools": [get_weather, get_weather_online, get_current_time],  # 完全替换:不含 calculator / 文件工具
    },
    {
        # 计算分析员:只会"算",不依赖任何查询工具
        "name": "calc-analyzer",
        "description": (
            "需要对数值做四则运算、批量计算或验算时使用。"
            "适合明确的算术任务,例如表达式求值、多组数字的汇总计算。"
        ),
        "system_prompt": (
            "你是计算分析专家。使用 calculator 工具完成所有运算,"
            "必要时拆成多步计算以保证准确。\n"
            "输出要求:先给出最终数值,再用一两句话说明计算过程,"
            "控制在 150 字以内;不要返回中间的原始工具输出。\n"
            "遇到无法用四则运算表达的问题时,明确说明无法计算及原因,"
            "不要硬算或编造结果。"
        ),
        "tools": [calculator],  # 完全替换:只保留计算器,最小权限
    },
]

subagents_demo.py

python 复制代码
# -*- coding: utf-8 -*-
"""
第 5 章示例入口:子 Agent 与上下文隔离(Context Quarantine)
============================================================

基于 deepagents (>= 0.7) 的字典方式定义两个简单子 Agent:
data-collector(数据收集)+ calc-analyzer(计算分析),
定义见 subagents.py;主 Agent 作为"协调者"把子任务委派出去。

对应教程:
    https://datawhalechina.github.io/deepagents-in-action/chapters/ch05-subagents/

代码拆分:
    subagents.py   两个子 Agent 的字典定义
    agent.py       单 Agent 版入口(本示例不含子 Agent)
    其余共享模块见 agent.py 顶部说明

运行前准备(同 agent.py):
    pip install -r requirements.txt
    cp .env.example .env   # 填入 LLM_API_KEY

运行:
    python subagents_demo.py

试试这些指令:
    查一下北京、上海、深圳的天气并对比,再算出三地平均气温乘 3 是多少
    计算 (128 + 472) * 3 - 100,并用一句话解释结果
"""

from deepagents import create_deep_agent

from model import build_model
# TodoListMiddleware / InMemorySaver 的导入路径随 deepagents 版本变化,
# 统一从 runtime 的兼容导入获取
from runtime import InMemorySaver, TodoListMiddleware, backend, chat_loop
from subagents import SUBAGENTS
from tools import calculator, get_current_time, get_weather


def build_agent():
    return create_deep_agent(
        model=build_model(),
        tools=[calculator, get_current_time, get_weather],  # 主 Agent 兜底工具
        backend=backend,  # 共用 Backend:子 Agent 写的文件主 Agent 可读
        subagents=SUBAGENTS,
        middleware=[TodoListMiddleware()],  # 仅主 Agent 生效,专业子 Agent 不继承
        checkpointer=InMemorySaver(),  # 第 4 章:todos 跨轮接续的关键
        system_prompt=(
            "你是一位项目协调者。面对用户任务时先规划再委派:\n"
            "1. 任务包含多个步骤时,先用 write_todos 制定任务清单,"
            "再开始委派与执行;每完成一步及时更新状态。\n"
            "2. 涉及查询多个天气/时间等信息源的,委派给 data-collector;\n"
            "3. 涉及数值计算、表达式求值的,委派给 calc-analyzer;\n"
            "4. 两者都涉及(如先查数据再计算),分别委派两个子 Agent,"
            "把收集结果转交给计算子 Agent;\n"
            "5. 单步的简单任务(只查一个城市、只算一个数)可以自己直接调用工具,"
            "不要制定计划,也不要为小事启动子 Agent。\n"
            "委派时在 task 的 description 里写清完整、可独立执行的任务说明,"
            "并附上子 Agent 需要的所有背景信息(它看不到当前对话)。\n"
            "如果子 Agent 返回的结果不完整或标注了失败,先判断能否自己用工具补齐,"
            "不能补齐就如实告知用户哪些部分未完成,不要编造。\n"
            "最后用中文整合子 Agent 的返回结果,给出简洁的最终答复。"
        ),
    )


def main() -> None:
    agent = build_agent()
    chat_loop(
        agent,
        thread_id="subagent-demo-1",
        banner=(
            "第 5 章示例:子 Agent 与上下文隔离 ------ 输入 exit 退出\n"
            "子 Agent: " + ", ".join(s["name"] for s in SUBAGENTS)
        ),
    )


if __name__ == "__main__":
    main()

tools.py

python 复制代码
# -*- coding: utf-8 -*-
"""
自定义工具集(@tool 装饰器,docstring 即工具说明)
==================================================

- calculator:安全的四则运算计算器(AST 白名单解析,不执行任意代码)
- get_current_time:查询当前时间
- get_weather:天气查询(模拟数据,可替换为真实 API)
- get_weather_online:真实天气查询(Tavily 联网搜索,需配置 TAVILY_API_KEY)

文件系统工具(read_file / write_file / ls / glob / grep 等)由
deepagents 内置的 FilesystemMiddleware 提供,无需在此定义。
"""

import os
from datetime import datetime

from langchain.tools import tool


@tool
def calculator(expression: str) -> str:
    """计算一个四则运算表达式并返回结果,例如 2 + 3 * 4 或 (10 - 2) / 3。"""
    import ast
    import operator

    _BINOPS = {
        ast.Add: operator.add,
        ast.Sub: operator.sub,
        ast.Mult: operator.mul,
        ast.Div: operator.truediv,
        ast.FloorDiv: operator.floordiv,
        ast.Mod: operator.mod,
        ast.Pow: operator.pow,
    }
    _UNARYOPS = {ast.USub: operator.neg, ast.UAdd: operator.pos}

    def _eval(node):
        if isinstance(node, ast.Expression):
            return _eval(node.body)
        if isinstance(node, ast.BinOp) and type(node.op) in _BINOPS:
            return _BINOPS[type(node.op)](_eval(node.left), _eval(node.right))
        if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARYOPS:
            return _UNARYOPS[type(node.op)](_eval(node.operand))
        if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
            return node.value
        raise ValueError(f"不支持的表达式元素: {ast.dump(node)}")

    try:
        result = _eval(ast.parse(expression.strip(), mode="eval"))
        return f"{expression} = {result}"
    except Exception as exc:  # noqa: BLE001
        return f"计算失败: {exc}"


@tool
def get_current_time() -> str:
    """获取当前的日期和时间。"""
    now = datetime.now()
    return f"当前时间: {now.strftime('%Y-%m-%d %H:%M:%S')} ({now.strftime('%A')})"


@tool
def get_weather(city: str) -> str:
    """查询指定城市的天气(示例为模拟数据,实际使用时可替换为真实 API 调用)。"""
    # 这里是演示用的模拟实现;接入真实 API 时,
    # 只需要把函数体替换成 requests.get(...) 之类的调用即可。
    mock = {
        "北京": "晴,26°C,微风",
        "上海": "多云,29°C,湿度 70%",
        "深圳": "阵雨,31°C,风力 3 级",
    }
    weather = mock.get(city, f"{city} 晴转多云,27°C(模拟数据)")
    return f"{city} 的天气: {weather}"


@tool
def get_weather_online(city: str) -> str:
    """联网查询指定城市今天的真实天气(基于 Tavily 搜索,需配置 TAVILY_API_KEY)。

    当用户需要真实、实时的天气信息时使用本工具;get_weather 只是模拟数据。
    """
    api_key = os.environ.get("TAVILY_API_KEY", "")
    if not api_key:
        return (
            f"无法联网查询 {city} 的天气:未配置 TAVILY_API_KEY 环境变量。"
            "请在 .env 中加入 TAVILY_API_KEY 后重启程序;"
            "或改用 get_weather 获取模拟数据。"
        )

    try:
        from tavily import TavilyClient

        client = TavilyClient(api_key=api_key)
        # search_depth="advanced" 走主索引,结果质量更稳;include_answer 让 Tavily 直接汇总
        response = client.search(
            f"{city} 今天天气 实时天气预报",
            search_depth="advanced",
            include_answer=True,
            max_results=5,
        )
    except ImportError:
        return (
            "无法联网查询天气:未安装 tavily-python。"
            "请在虚拟环境中执行 pip install tavily-python 后重试。"
        )
    except Exception as exc:  # noqa: BLE001  网络/鉴权等异常都如实回报
        return f"联网查询 {city} 天气失败: {exc}"

    answer = (response.get("answer") or "").strip()
    if not answer:
        return f"未能从搜索结果中获取 {city} 的天气摘要,请改用其他方式查询。"

    # 附上来源标题,便于追溯;不堆砌原始结果,保持返回精简
    sources = []
    for item in response.get("results", [])[:3]:
        title = (item.get("title") or "").strip()
        if title:
            sources.append(title)
    source_line = f"(来源: {'; '.join(sources)})" if sources else ""
    return f"{city} 的天气: {answer} {source_line}".strip()


# 供组装时统一引用
CUSTOM_TOOLS = [calculator, get_current_time, get_weather, get_weather_online]

agent.py

python 复制代码
# -*- coding: utf-8 -*-
"""
主入口:单 Agent 版(deepagents >= 0.7)
========================================

功能演示:
1. 自定义工具(tools.py):计算器 / 当前时间 / 模拟天气查询
2. deepagents 内置的虚拟文件系统工具(read_file / write_file 等)
3. 任务规划(第 4 章):TodoListMiddleware 显式开启 + InMemorySaver 检查点,
   使 todos 任务清单跨轮对话接续(同一 thread_id),每轮结束打印清单进度
4. 自定义 Skill(渐进式披露):skills/ 目录下的 report-writer 报告撰写规范

代码拆分(避免单文件堆积):
    model.py       模型配置(环境变量 / OpenAI 兼容接口)
    tools.py       自定义工具集
    runtime.py     文件系统后端 + 对话循环 + 输出打印
    agent.py       本文件:组装主 Agent 并启动(单 Agent 版)

运行前准备:
    pip install -r requirements.txt
    cp .env.example .env   # 然后填入你的 API Key

运行:
    python agent.py
"""

from deepagents import create_deep_agent

from model import build_model
# TodoListMiddleware / InMemorySaver 的导入路径随 deepagents 版本变化,
# 统一从 runtime 的兼容导入获取
from runtime import InMemorySaver, SKILLS_DIR, TodoListMiddleware, backend, chat_loop
from tools import CUSTOM_TOOLS


def build_agent():
    return create_deep_agent(
        model=build_model(),
        tools=CUSTOM_TOOLS,
        backend=backend,
        skills=[SKILLS_DIR],  # 挂载 skills/ 目录下全部 Skill
        middleware=[TodoListMiddleware()],  # 0.7 起:按需显式开启任务规划
        checkpointer=InMemorySaver(),  # 第 4 章:todos 跨轮接续的关键
        system_prompt=(
            "你是一个乐于助人的通用助手,可以调用工具完成计算、查时间、查天气,"
            "也可以使用文件工具读写当前目录下的文件。"
            "当用户请求与某个 Skill 的 description 匹配时(例如写报告、总结、周报),"
            "先用 read_file 读取该 Skill 的 SKILL.md 全文,并严格按其中的规范执行。\n"
            "任务规划要求(多步骤任务必须遵守):\n"
            "1. 遇到包含 2 个及以上步骤的任务时,先调用 write_todos 制定任务清单,"
            "再开始执行,避免遗漏或重复;\n"
            "2. 开始某一步前把它标为 in_progress,完成后标为 completed,"
            "发现新需求时动态补充任务;\n"
            "3. 单步简单问题(只查一个值、只算一个数)不要制定计划,直接回答。\n"
            "最后用中文给出简洁的总结。"
        ),
    )


def main() -> None:
    agent = build_agent()
    # 配置了 Checkpointer 后,每轮只传新的用户输入,
    # 历史消息和 todos 都由检查点按 thread_id 自动接续
    chat_loop(
        agent,
        thread_id="demo-session-1",
        banner="简单 Deep Agent(deepagents >= 0.7)------ 输入 exit 退出",
    )


if __name__ == "__main__":
    main()

app.py

python 复制代码
# -*- coding: utf-8 -*-
"""
Streamlit 前端:Deep Agent 可视化界面
=====================================

封装本项目已有的全部能力:
- 两种模式切换:单 Agent(agent.py)/ 子 Agent 协作(subagents_demo.py)
- 多轮对话(Checkpointer 按 thread_id 接续,同一会话内历史与 todos 不丢)
- 任务清单进度(第 4 章 write_todos:pending / in_progress / completed)
- 工具调用轨迹(每轮调了什么工具、参数、结果)
- 子 Agent 委派可视化(task 工具调用即委派,单独标注)
- 工作区文件浏览(FilesystemBackend 根目录,Agent 写的文件可直接查看)

运行前准备:
    pip install -r requirements.txt   # 已含 streamlit
    cp .env.example .env              # 填入 LLM_API_KEY

启动:
    streamlit run app.py
"""

import os

import streamlit as st

from agent import build_agent as build_single_agent
from runtime import ROOT, print_todos
from subagents_demo import build_agent as build_subagent_agent
from subagents import SUBAGENTS

PAGE_TITLE = "Deep Agent 演示台"
MODE_SINGLE = "单 Agent(工具 + Skill + 任务规划)"
MODE_SUBAGENT = "子 Agent 协作(协调者 + 两个专项子 Agent)"

# ----------------------------------------------------------------------
# 页面与全局配置
# ----------------------------------------------------------------------
st.set_page_config(page_title=PAGE_TITLE, page_icon="🤖", layout="wide")

st.title("🤖 Deep Agent 演示台")
st.caption(
    "基于 deepagents >= 0.7 | 多轮对话 · 任务规划 · 子 Agent 委派 · 文件系统"
)


# ----------------------------------------------------------------------
# 会话状态初始化(Streamlit 每次交互都会重跑脚本,状态放 session_state)
# ----------------------------------------------------------------------
def init_state() -> None:
    st.session_state.setdefault("mode", MODE_SINGLE)
    st.session_state.setdefault("agents", {})  # {mode: agent},按需构建、复用
    st.session_state.setdefault("thread_ids", {MODE_SINGLE: "ui-single-1", MODE_SUBAGENT: "ui-sub-1"})
    st.session_state.setdefault("history", [])  # 展示用:[{role, content, tools, todos}]


init_state()

# ----------------------------------------------------------------------
# 侧边栏:模式切换 / 会话管理 / 子 Agent 说明 / 文件浏览
# ----------------------------------------------------------------------
with st.sidebar:
    st.header("⚙️ 设置")

    mode = st.radio(
        "Agent 模式",
        [MODE_SINGLE, MODE_SUBAGENT],
        index=[MODE_SINGLE, MODE_SUBAGENT].index(st.session_state.mode),
        help="切换后各自维护独立的对话线程,互不干扰",
    )

    # 模式切换时,重建 agent(同一会话内线程也独立,历史按模式分别保存)
    if mode != st.session_state.mode:
        st.session_state.mode = mode
        st.rerun()

    if st.button("🗑️ 新建会话(清空当前模式历史)"):
        key = "ui-" + ("single" if mode == MODE_SINGLE else "sub") + "-" + str(len(st.session_state.thread_ids))
        st.session_state.thread_ids[mode] = key
        st.session_state.history = []
        st.session_state.agents.pop(mode, None)
        st.rerun()

    st.divider()

    if mode == MODE_SUBAGENT:
        st.subheader("🧩 当前子 Agent")
        for s in SUBAGENTS:
            tools = ", ".join(t.name for t in s["tools"])
            with st.expander(f"**{s['name']}**"):
                st.markdown(f"**职责**:{s['description']}")
                st.caption(f"可用工具:{tools}")

    st.subheader("📁 工作区文件")
    st.caption("FilesystemBackend 根目录(Agent 写的文件会出现在这里)")
    try:
        entries = sorted(os.listdir(ROOT))
        files = [e for e in entries if os.path.isfile(os.path.join(ROOT, e))]
        dirs = [e for e in entries if os.path.isdir(os.path.join(ROOT, e))]
        picked = st.selectbox("选择文件查看", files or ["(暂无文件)"])
        if picked and picked != "(暂无文件)":
            try:
                with open(os.path.join(ROOT, picked), encoding="utf-8") as f:
                    st.code(f.read()[:8000], language="text")
            except (UnicodeDecodeError, OSError):
                st.warning("该文件不是可显示的文本文件")
        if dirs:
            st.caption(f"子目录:{', '.join(dirs)}")
    except OSError as exc:
        st.warning(f"无法读取工作区:{exc}")

# ----------------------------------------------------------------------
# 构建当前模式的 Agent(每个模式只构建一次,缺 Key 时给出友好提示)
# ----------------------------------------------------------------------
def get_agent():
    agents = st.session_state.agents
    if mode not in agents:
        try:
            builder = build_single_agent if mode == MODE_SINGLE else build_subagent_agent
            with st.spinner("正在初始化 Agent(加载模型 / Skill / 子 Agent)..."):
                agents[mode] = builder()
        except RuntimeError as exc:  # 未配置 LLM_API_KEY 等
            st.error(f"初始化失败:{exc}")
            st.stop()
    return agents[mode]


# ----------------------------------------------------------------------
# 对话主区域:渲染历史 + 输入框
# ----------------------------------------------------------------------
def render_history() -> None:
    for item in st.session_state.history:
        role = item["role"]
        if role == "user":
            with st.chat_message("user"):
                st.markdown(item["content"])
        else:
            with st.chat_message("assistant"):
                # 工具调用 / 委派轨迹(折叠展示,不干扰阅读)
                for trace in item.get("tools", []):
                    kind = trace.get("kind", "tool")
                    icon = "🧩" if kind == "subagent" else "🔧"
                    label = f"{icon} {trace['name']}  `{trace['args']}`"
                    with st.expander(label):
                        st.code(trace.get("result", ""), language="text")
                if item.get("content"):
                    st.markdown(item["content"])
                # 任务清单进度
                todos = item.get("todos") or []
                if todos:
                    icons = {"pending": "⬜", "in_progress": "🔄", "completed": "✅"}
                    st.markdown("**📋 任务清单**")
                    for t in todos:
                        st.markdown(f"{icons.get(t.get('status'), '❓')} {t.get('content', '')}")


render_history()

user_input = st.chat_input("输入任务,例如:查北京上海深圳的天气并对比,再算温差乘 5")

if user_input:
    agent = get_agent()
    # 先渲染用户消息,再等待执行
    st.session_state.history.append({"role": "user", "content": user_input})
    with st.chat_message("user"):
        st.markdown(user_input)

    with st.chat_message("assistant"):
        with st.spinner("Agent 执行中..."):
            try:
                result = agent.invoke(
                    {"messages": [("user", user_input)]},
                    config={"configurable": {"thread_id": st.session_state.thread_ids[mode]}},
                )
            except Exception as exc:  # noqa: BLE001
                st.error(f"执行出错:{exc}")
                st.session_state.history.append({"role": "assistant", "content": f"⚠️ 执行出错:{exc}"})
                st.stop()

        # 从本轮消息里提取工具调用轨迹(task 调用 = 子 Agent 委派)
        traces = []
        for message in result["messages"]:
            mtype = getattr(message, "type", "")
            if mtype == "ai":
                for call in getattr(message, "tool_calls", None) or []:
                    traces.append(
                        {
                            "kind": "subagent" if call["name"] == "task" else "tool",
                            "name": call["name"],
                            "args": call["args"].get("description", call["args"]),
                            "result": "",
                        }
                    )
            elif mtype == "tool" and traces:
                traces[-1]["result"] = str(getattr(message, "content", ""))[:2000]

        # 最终回复 = 最后一条 AI 消息的文本
        final_text = ""
        for message in reversed(result["messages"]):
            if getattr(message, "type", "") == "ai" and getattr(message, "content", ""):
                final_text = message.content
                break

        todos = result.get("todos") or []

        # 渲染 + 存入历史(Streamlit 下轮重跑时靠 history 重新渲染)
        for trace in traces:
            icon = "🧩" if trace["kind"] == "subagent" else "🔧"
            with st.expander(f"{icon} {trace['name']}  `{str(trace['args'])[:120]}`"):
                st.code(trace["result"], language="text")
        if final_text:
            st.markdown(final_text)
        if todos:
            icons = {"pending": "⬜", "in_progress": "🔄", "completed": "✅"}
            st.markdown("**📋 任务清单**")
            for t in todos:
                st.markdown(f"{icons.get(t.get('status'), '❓')} {t.get('content', '')}")

        st.session_state.history.append(
            {
                "role": "assistant",
                "content": final_text,
                "tools": traces,
                "todos": todos,
            }
        )

    # 侧边栏文件列表可能已变化,重跑一次以刷新
    st.rerun()

参考教程

https://datawhalechina.github.io/deepagents-in-action

相关推荐
焱焱枫3 小时前
OceanBase 为什么要多一层 Unit 和 ResourcePool?Tenant 直接分 CPU 内存不行吗?多租户资源模型一次讲透
经验分享
卷无止境3 小时前
便宜模型和贵模型到底差在哪儿?一份关于Claude与GPT分级体系的深度梳理
后端·python
玩美移动3 小时前
AI Skin Analysis API 技术解析:文件上传、异步任务与结果读取
java·人工智能·python
艾莉丝努力练剑3 小时前
【AI大模型接入SDK】ChatSDK整体实现
网络·c++·人工智能·学习·架构
励志不掉头发的内向程序员3 小时前
【从零写一个CAD 02】画完第一条线之后:实体容器、Esc 取消,和按了没反应的键盘
开发语言·c++·qt·学习·系统架构
LCDduhui3 小时前
LCD屏幕中常见的液晶屏驱动芯片有哪些
经验分享
無a伟3 小时前
彻底搞懂ToolCalling、MCP,Skills的核心区别,能力上的层层封装
大数据·agent·mcp·toolcalling·skills
摇滚侠3 小时前
《On Java 中文版 基础卷》阅读笔记 安装 Java 和本书示例 02
java·开发语言·笔记
七夜zippoe4 小时前
RAG 2.0:从向量检索到 Agent 自主知识治理的进化路径
ai·agent·向量检索·rag 2.0·自主知识治理