# deep_agent 2.0 基于 [deepagents](https://pypi.org/project/deepagents/) 的深度智能体服务骨架。 主入口是 `POST /api/chat`,返回 SSE 流。 **对外契约与 1.5 完全兼容**:`POST /api/chat` 的 multipart 入参、SSE 帧格式、 错误码语义都逐字对齐,1.5 的 `static/index.html` 不改一行即可连上。 **对内实现全部重写**:1.5 的业务工具、业务技能、MCP 工具池不在本仓库范围内。 骨架提供的是结构与纪律,附 4 个演示工具与 1 个示例技能示范扩展方式。 --- ## 快速开始 ```bash # 1) 依赖 pip install -r requirements.txt # 2) 配置 cp .env.example .env # 至少填 OPENAI_API_KEY 与 OPENAI_BASE_URL # 想用真实 MySQL:把 DB_DIALECT 改成 mysql 并填 DB_* 连接信息 # (库不存在时启动会自动建库,不用手工 CREATE DATABASE) # 3) 跑起来 python main.py # 或 bash scripts/start_server.sh start (生产,带 PID 文件与优雅停止) # 4) 打开测试页 # http://127.0.0.1:8080 ``` 开箱配置走 **SQLite 开发档**(`DB_DIALECT=sqlite`),无需任何数据库服务即可 跑起来。生产/联调改 `DB_DIALECT=mysql`。 > **不能与 1.5 共用同一个 Python 环境**:2.0 绑定 langchain 1.3 / langgraph 1.2 / > deepagents 0.6.12,与 1.5 声明的 `langchain>=0.3` 系列不同代,装在一起必然冲突。 > 用独立 venv。 --- ## 服务端口 **8080**(1.5 是 8070,避开冲突)。改 `PORT` 即可。 --- ## API 主入口与 1.5 一致,额外新增了若干辅助接口。 ### 对话 | 方法 | 路径 | 说明 | |---|---|---| | POST | `/api/chat` | 主入口,SSE 流式 | | POST | `/api/chat/resume` | 恢复被中断挂起的对话 | | GET | `/api/chat/interrupt` | 查挂起的中断(刷新页面补渲染卡片);有则 200,无则 204 | | GET | `/api/chat/history/{session_id}` | 会话历史 | > **没有 `/api/chat/only`**:1.5 有这个端点,但它与 `/chat` 除了流水上的 > `kind` 标签外行为完全相同,而那个标签没有任何消费方(不聚合、不分账、 > 不影响额度),1.5 前端也从未调用过它。2.0 删掉了它——理由与 1.5 为此付过的 > 代价见 `AGENTS.md` §六。 `POST /api/chat` 请求体是 `multipart/form-data`: | 字段 | 必填 | 说明 | |---|---|---| | `message` | 是 | 用户消息,支持 `#skill:` 与 `#session:` 前缀 | | `session_id` | 否 | 缺省则新建会话 | | `thread_id` | 否 | **必须等于 `session_id`**,否则 404 | | `mode` | 否 | `plan`(先出计划待批)或 `full`(直接执行) | | `file` | 否 | 附件,落到 `/workspace/uploads/` | | `user_name` | 否 | 归属用户,缺省 `default`(**2.0 新增,替代 1.5 的鉴权依赖**) | 响应是 `text/event-stream`,头含 `X-Session-Id` 与 `X-Trace-Id`。 ### 会话与用量 | 方法 | 路径 | 说明 | |---|---|---| | GET | `/api/sessions` | 会话列表(置顶优先) | | GET | `/api/sessions/search?q=` | 标题模糊搜索 | | DELETE | `/api/sessions/{id}` | 删除会话(**同时清图状态**) | | DELETE | `/api/messages/{id}` | 删除单条消息 | | GET/PUT | `/api/sessions/{id}/mode` | 权限模式 | | PUT | `/api/sessions/{id}/pin` · `/title` | 置顶 · 重命名 | | GET | `/api/sessions/{id}/context` | 上下文用量明细与压缩存档索引 | | GET | `/api/usage/today` | 当日用量与额度 | | GET | `/api/usage/recent` | 最近用量流水 | | PUT | `/api/usage/quota` | 调整每日额度(0 = 不限) | | GET | `/api/usage/pricing` | 模型单价表 | | GET | `/api/preferences` · PUT · DELETE | 用户偏好(每轮自动注入) | ### 模型与工具 | 方法 | 路径 | 说明 | |---|---|---| | GET | `/api/model` | 模型配置与 agent 组装摘要 | | GET | `/api/model/tools` | 工具清单(按分组) | | POST | `/api/model/thinking` | 切换思考级别(触发 agent 重建) | | POST | `/api/model/reload` | 强制重建 agent | ### 定时任务 | 方法 | 路径 | 说明 | |---|---|---| | GET/POST | `/api/schedules` | 列表 / 新建 | | GET/PUT/DELETE | `/api/schedules/{id}` | 详情 / 更新 / 删除 | | POST | `/api/schedules/{id}/pause` · `/resume` · `/run` | 暂停 / 恢复 / 立即执行 | | GET | `/api/schedules/{id}/runs` | 执行记录 | ### 运维 | 方法 | 路径 | 说明 | |---|---|---| | GET | `/health` | 逐子系统健康状态 | | GET | `/api/health/billing` | 计费自检(单价是否覆盖当前模型) | | GET | `/` | 重定向到测试页 | | GET | `/static/index.html` | 极简测试页 | --- ## SSE 事件契约 14 种业务事件 + 1 种通用错误。**字段名是从 1.5 前端逐行反查出来的**, 不是猜的。 | 事件 | 前端读取的字段 | |---|---| | `agent_start` | `agent` · `cn_agent` | | `thinking` | `message` · `cn_agent` | | `thinking_token` | `content` | | `executing` | `message` · `cn_agent` · `tools[]` · `cn_tools[]` | | `tool_call` | `tool` · `cn_tool` · `args` · `agent_id` | | `tool_result` | `tool` · `cn_tool` · `content` · `error` · `agent_id` | | `token` | `content` | | `updated_todo_list` | `todos` | | `progress` | `message` | | `interrupt` | `tool_approval{tool,args,reason,side_effect}` 或 `ask_user{questions[]}` 或 `message` | | `citations` | `items[]` · `model_only` | | `agent_done` | `agent` · `cn_agent` · `duration_ms` · `progress` · `context_tokens` | | `agent_error` | `agent` · `cn_agent` · `message` · `duration_ms` | | `done` | `duration_ms` · `usage{}` | | `error` | `message` · `code` · `retry_after` | 帧格式:`data: {json}\n\n`,每帧恒带 `session_id`,绑定 trace 时带 `trace_id`。 HTTP 错误语义:额度超限 429;并发超限 429(带 `retry_after`);上传 400/413; 会话不存在或越权 404;恢复时无挂起中断 409。 **契约的唯一出口是 `api/sse_events.py`**,其他任何地方都不得手拼 `data: ` 字符串。改字段必须同步改 `tests/test_sse_contract.py`。 --- ## 四项核心能力(相对 1.5 的做法改进) 这是 2.0 与 1.5 的主要差异所在。 ### 子智能体跟踪 | | 做法 | |---|---| | 1.5 | 从子图命名空间字符串 `tools:` **反查** `subagent_type`;id 被上下文压缩裁掉后反查落空,子智能体身份丢失 | | 2.0 | 收到 task 调用的**那一刻**就建 `tool_call_id → {类型, 描述}` 表,之后只按 id 查表;表由跟踪器持有,不受历史窗口影响 | 见 `api/subagent_tracker.py`。 ### 上下文计量 | | 做法 | |---|---| | 1.5 | tiktoken **本地估算**优先,为此专门开了 `fix/context-usage-accounting` 分支修口径 | | 2.0 | **真实 usage 优先**(`usage_metadata`),provider 不给才回落估算并标 `usage_source=estimate` | 两个量刻意分开:`input_tokens` 累加值是计费口径,`context_tokens`(最近一次 调用的 input)是当前上下文占用。 见 `api/context_meter.py`。 ### 自动压缩 | | 做法 | |---|---| | 1.5 | 自研 `auto_compact.py`,默认**关闭**,未被信任到敢开 | | 2.0 | 直接用 deepagents 的 `SummarizationMiddleware`:85% 窗口触发,**把被淘汰的历史追加落盘**到 `/conversation_history/{thread_id}.md` 并把路径嵌进摘要,agent 能自己读回来 | 调节旋钮是 `CONTEXT_LIMIT`(声明窗口),不是阈值比例——阈值由框架固定为 85%。压缩存档位置可在 `GET /api/sessions/{id}/context` 的 `archives` 里查。 ### 来源标注 | | 做法 | |---|---| | 1.5 | 靠提示词让**模型自觉**在正文写引用,再用启发式规则去正文里刮 | | 2.0 | **工具侧结构化产出**:工具返回 `sourced(data, sources)`,SSE 层从 ToolMessage 抽取、聚合、去重 | 模型漏写和编造这两种失败模式,在读工具返回值的路径上根本不存在。 见 `api/source_tracker.py` 与 `tools/registry.py`。 --- ## 扩展方式 ### 加一个业务工具 ```python # tools/my_tools.py from tools.registry import register, source, sourced @register(group="我的分组", label="中文显示名", summary="一句话说明") def my_tool(query: str, limit: int = 5) -> dict: """做某事。 Args: query: 查询词。 limit: 最多返回几条。 Returns: 含 data 与 sources 的结构;sources 会被渲染成引用标注。 """ return sourced( data=[...], sources=[source("标题", "https://...", "摘要", score=0.9)], ) ``` 然后把模块名加进 `tools/registry.py` 的 `_BUILTIN_MODULES`。工具名全局唯一, 重名会在启动时抛错。 **docstring 是给模型看的**:第一行说清做什么,正文说清何时用、参数含义、 返回什么。模型选错工具多半是因为 docstring 写得含糊。 ### 加一个技能 在 `skills//SKILL.md` 写带 frontmatter 的操作手册: ```markdown --- name: my-skill description: 什么时候该用这个技能(这段会被模型看到,要写具体) --- # 标题 ## 步骤 1. ... ``` agent 启动时只读 frontmatter,按需才 `read_file` 读全文(渐进披露)。 用户用 `#skill:my-skill` 前缀显式指定。 ### 加一个子智能体 改 `agents/agent_factory.py` 的 `_build_subagents()`。注意 `tools` 必须是 `BaseTool` 列表(用 `registry.get_tools_by_name(...)`),`system_prompt` 不继承父级。 --- ## 配置 全部配置项见 `.env.example`,每项都有默认值。几个容易搞错的: | 变量 | 说明 | |---|---| | `DEEP_AGENT_ENV_PRIORITY` | `.env` 与进程环境谁优先。`dotenv`(默认)/ `process` / `none`。**跑变体实例用 `process`**:`DEEP_AGENT_ENV_PRIORITY=process DB_DIALECT=mysql PORT=8081 python main.py` | | `DB_DIALECT` | `mysql`(生产)/ `sqlite`(本地开发与测试,无需数据库服务) | | `DB_NAME` | MySQL 库名;库不存在时启动会自动建库,不需要手工 `CREATE DATABASE` | | `CONTEXT_LIMIT` | 声明的上下文窗口,**压缩的唯一调节旋钮**。0 = 用模型登记值 | | `CTX_COMPACT_ENABLED` | 关掉会同时失去历史落盘能力,长会话会超窗报错 | | `CHECKPOINTER` | `sqlite`(默认,落盘)/ `memory`(重启丢图状态) | | `HITL_TOOLS` | 追加需要人工审批的工具名,逗号分隔。如 `calculate` 便于自测中断闭环 | | `BILLING_ALLOW_UNPRICED` | 模型未定价时是否按 0 计费。**默认 0 = 直接报错**(fail-closed) | | `TFAGENTS_DAILY_TOKEN_LIMIT` | 每用户每日 token 限额,0 = 不限 | **配置冲突会被大声报出来**:若 `.env` 与进程环境里有同名但不同的值,启动 日志会打 WARNING(含脱敏后的生效值),`/health` 的 `config_conflicts` 也能 看到。这解决的是一个很费时间的现象:改了配置却不生效,却没有任何提示。 --- ## 验证 ```bash # 离线单元 + SSE 契约测试(不需要密钥,不需要数据库服务) python -m pytest # 结构自检:导入、建表、agent 编译、工具注册 python scripts/selfcheck.py # HTTP 层端到端(需服务已启动 + 可用密钥) python scripts/http_e2e.py ``` `http_e2e.py` 覆盖 49 项断言:SSE 帧格式与响应头、落库与顺序、错误语义 (404/400/409)、**中断/恢复闭环**、定时任务的手动触发与 cron 校验。 --- ## 目录结构 ``` main.py FastAPI 入口(lifespan / 健康检查 / 静态挂载) config.py 配置唯一入口(.env 读取与冲突检测) api/ chat · sse_core · session · model · usage · schedule · concurrency sse_events.py ★ SSE 契约唯一事实源 sse_core.py ★ 流式内核(读流 → 派事件 → 收尾) subagent_tracker.py 子智能体生命周期跟踪 context_meter.py 上下文计量(真实 usage 优先) source_tracker.py 来源标注抽取与聚合 agents/ agent_factory · agent_registry · model_factory · prompts · middleware · agent_memory tools/ registry(工具唯一事实源)· demo_tools(示例工具) skills/demo-skill/ 示例技能 db/ db_conn(双方言)· mysql_schema.sql · schema_sqlite.sql · 三个 store billing/ pricing · guard(预检)· meter(入账) scheduler/ scheduler(APScheduler)· runner(独立会话执行) obs/ log_setup · tracing(trace_id) static/index.html 极简测试页 tests/ pytest(141 项,离线可跑) scripts/ selfcheck.py · http_e2e.py · start_server.sh ``` 标 ★ 的两个文件是核心,改动前先读 `AGENTS.md`。 --- ## 已知缺口 **当前只适合内网或开发环境**,主要因为: 1. **没有鉴权** —— `user_name` 由客户端自报,可伪造。计费与会话归属都建立在 这个值上。接真实鉴权只需替换 `api/chat_routes._resolve_user`。 2. **单进程** —— SQLite checkpointer、并发计数、调度器都是进程内的。多 worker 会导致会话状态分裂、定时任务重复触发。 3. **并发计数不跨进程** —— 多进程部署需挪到 Redis/MySQL。 其余有意留空的部分(无业务工具/技能、无 MCP、标题用截断、`remember` 未实现) 见 `AGENTS.md` §六。 工程纪律、踩过的 11 个坑、分层依赖规则见 **[AGENTS.md](AGENTS.md)**。