个人主页:https://github.com/zbhgis/
打卡记录
项目演示
1.工具调用能力

2.skill调用

3.任务规划

4.工作区文件保存

其他能力具体可自行启动项目运行体验
langsmith记录
1.工具调用能力

2.skill调用

3.任务规划

4.工作区文件保存

完整代码
仓库:https://github.com/zbhgis/simple_agent_demo
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,
)
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)
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()
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]
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()
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()