DEVELOPMENT.md 15 KB

开发参考

API、SSE、扩展方式和框架约束。运行、调试、打包与部署见 项目 README。以下源码命令均在项目根目录执行,使用项目虚拟环境。

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,统一使用 /api/chat。

POST /api/chat 请求体是 multipart/form-data:

字段 必填 说明
message 是 用户消息,支持 #skill:<name> 与 #session:<uuid> 前缀
session_id 否 缺省则新建会话
thread_id 否 必须等于 session_id,否则 404
mode 否 plan(先出计划待批)或 full(直接执行)
file 否 附件,落到 /workspace/uploads/
request_id 否 客户端生成的请求唯一键;同一用户重复提交返回 409 和原 session_id、run_id
user_name 否 归属用户,缺省 default(2.0 新增,替代 1.5 的鉴权依赖)

响应是 text/event-stream,头含 X-Session-Id、X-Trace-Id 与 X-Run-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}/runs 最近 50 次执行的状态与错误
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:<X> 反查 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。


扩展方式

加一个业务工具

# 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/<name>/SKILL.md 写带 frontmatter 的操作手册:

---
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 也能 看到。这解决的是一个很费时间的现象:改了配置却不生效,却没有任何提示。


验证

# 离线单元 + SSE 契约测试(不需要密钥,不需要数据库服务)
python -m pytest

# 结构自检:导入、建表、agent 编译、工具注册
python scripts/selfcheck.py

# HTTP 层端到端(需服务已启动 + 可用密钥)
python scripts/http_e2e.py

http_e2e.py 覆盖: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(离线单元、HTTP 与真实图引擎回归)
scripts/                   selfcheck.py · http_e2e.py · start_server.sh

标 ★ 的两个文件是核心,改动时同步检查 SSE 契约与流式执行测试。


执行可靠性与隔离

  • 统一准入:聊天、恢复和定时执行共用额度预检、并发闸、会话互斥、总超时、计量与收尾。并发满返回 429;同会话冲突返回 409。
  • 运行状态:execution_runs 保存 running / waiting_input / success / failed / cancelled / interrupted。中断只表示等待输入,不发送完成事件;启动时将崩溃遗留的 running 标记为 interrupted。
  • 请求去重:聊天可提交 request_id(1–128 字符)。相同用户的相同键不会再次运行,返回 409 和原会话、运行 ID;不重放旧 SSE。新任务应使用新键。
  • 取消收尾:断连时停止消费图,保存已收到的部分回答、结算已观测用量,并释放占位。定时任务停机时等待取消收尾后再关闭 checkpoint。
  • 持久化结算:同一流水使用稳定 ID,写账与累计额度在事务中完成。待结算数据保存在 billing_pending,服务启动后及每分钟重试。数据库完全不可写时仍只能日志告警;进程被强杀前尚未观测到的 provider 用量需要外部账单对账。
  • 文件隔离:虚拟 /workspace/、/conversation_history/ 映射到 data/workspaces/<用户哈希>/<会话哈希>/。子智能体共享其父会话目录,其他会话不可见;/skills/ 共享只读。
  • 定时任务:采用同一执行流程,注入模式和用户偏好;cron 与手动触发共用计划互斥,等待审批的运行记录为 waiting_input,从聊天恢复后同步结果状态。cron 显式使用 Asia/Shanghai。

Agent 人机交互

  • ask_user(questions):提出一至三个必要问题并暂停,前端显示输入框;刷新页面可恢复问题卡。
  • request_plan_approval(plan):提交计划等待确认。plan 模式提示词会准确指向这个工具。
  • 批量工具审批卡完整展示每项工具及参数,批准按钮明确写出批次数量。
  • 审批卡带 interrupt_token;恢复接口必须原样携带。服务端在会话锁内核对当前中断,过期、缺失和重复确认返回 409。
  • 业务工具可在 @register(..., requires_approval=True) 声明强制审批,组装时自动进入框架 HITL。plan 提示词本身不构成所有工具的硬权限边界,真实写操作应声明强制审批。
  • 子智能体和摘要模型的用量计入总费用,其内部正文不混入主回答,也不覆盖主线程上下文占用。

升级已有部署

启动自动增建 execution_runs、billing_pending、request_keys,不会删除旧表。 同步发布 static/app.js;自定义客户端需补传 interrupt_token。定时任务的详情、修改、删除、运行接口需携带其归属 user_name。

旧的共享 workspace/、conversation_history/ 原样保留,不会自动猜测文件归属或复制到新目录。 旧会话需要的附件可重新上传;需要迁移的历史文件应先确认用户和会话归属。 未登记的新模型现在会在执行前拒绝请求,不再自动写入零价;既有单价记录不会被覆盖。

已知缺口

当前只适合内网或开发环境,主要因为:

  1. 没有鉴权 —— user_name 由客户端自报,可伪造。计费与会话归属都建立在 这个值上。需要为所有路由接入统一身份依赖,并区分普通用户与管理操作。
  2. 单进程 —— SQLite checkpointer、并发计数、调度器都是进程内的。多 worker 会导致会话状态分裂、定时任务重复触发。
  3. 并发计数不跨进程 —— 多进程部署需挪到 Redis/MySQL。

尚未提供业务工具/技能、MCP 接入及 remember;默认标题使用文本截断。