③ 问答
Agent 类型
DeepAgent 与 BaseAgent:tool loop 在谁那边运行、各自怎么组织一轮对话,以及它们的 provider、prompt 和工具怎么配
随包只有两种 agent 运行时,而它们的区别不在体量,而在tool loop 在谁那边运行。两者读取的是
同一份工具注册表,应答的也是同一个 POST /api/chat。
| DeepAgent:引擎在你的部署中运行 | BaseAgent:由你的模型来调用 | |
|---|---|---|
| tool loop 运行在 | 这里,你的部署里 | LLM 服务商那边,通过 HTTP 调用 /mcp |
| 骨架 | LangChain create_agent + deepagents 中间件栈 | 没有,故意的 |
| 虚拟文件系统 | 有,由 PostgreSQL 支撑 | 没有 |
| 代码执行 | eval(进程内 JavaScript REPL) | 没有 |
| Agent Skills | 有,走 SkillsMiddleware | 没有 |
| 图表 | 一段 vis-chart 围栏块,由客户端渲染 | 没有,由消费它的客户端自带 |
| provider 配置键 | PROVIDERS_DEEP | PROVIDERS_BASE |
| 什么时候用 | 默认选择,没有特别理由就用它 | 你的目标是 Claude Desktop、Cursor、ChatGPT Apps 或任意 MCP 客户端 |
DeepAgent 是主力:读文件、做计算、多步工具调用都在它这边。BaseAgent 则刻意做成 MCP 工具面 之上最薄的一层派生,其能力与外部 MCP 客户端所能获得的能力完全一致:凡是 BaseAgent 独力 做不到的事,外部 MCP 客户端也一样做不到。
每轮对话的 agent 选择
Section titled “每轮对话的 agent 选择”agent 的发现方式和工具一样:启动时扫目录。
-
AGENT_DIRS里的每个目录都被扫一遍.py文件 子目录、以及以_开头的文件名都跳过 - 一个类只要定义了
generate_response就成为 agent 类名去掉结尾的Agent就是 agent 名,如DeepAgent→Deep - 如果它同时定义了
load_llm_clients,此刻就会执行 以PROVIDERS_{NAME}为输入,每个 provider 条目建一个 client - 一个 client 都没有的 agent 不会被提供 所以把
PROVIDERS_BASE留空,就是关掉 BaseAgent 的办法 -
GET /api/models返回活下来的agent/provider组合 而POST /api/chat把agent和provider作为两个字段分别接收
curl http://localhost:18080/api/models{"success":true,"code":0,"msg":"ok","data":["Base/gemini-2.5-flash","Deep/claude-sonnet","Deep/gemini-3.5-flash"]}也就是说两半都由客户端选。不传 provider 时,DeepAgent 回退到 DEFAULT_PROVIDER_DEEP,
该项未设则回退到 gemini-3.5-flash;BaseAgent 回退到 gemini-2.5-flash。
DeepAgent
Section titled “DeepAgent”DeepAgent 每轮用四项输入即时组装一个 deepagents agent:发现到的工具、一份 system prompt、
一个由 PostgreSQL 支撑的文件系统,以及一层中间件栈。
虚拟文件系统
Section titled “虚拟文件系统”对已认证用户来说,后端是一个五挂载点的 CompositeBackend。deepagents 原生的文件
工具(ls、read_file、write_file、edit_file、glob、grep)把它当成一块磁盘
那样操作:
| 挂载点 | 作用域 | 权限 | 放什么 |
|---|---|---|---|
| (默认) | 本会话 | 读写 | 本轮的草稿空间 |
/memories/ | 跨会话 | 读写 | agent 自己决定留下的笔记 |
/uploads/ | 本会话 | 只读 | 本次请求附带的文件 |
/library/ | 跨会话 | 只读 | 该用户更早解析过的文件 |
/skills/ | 随包 | 只读 | 来自 SKILL_DIRS 的 Agent Skills,agent 绝不允许改自己的 skill |
/uploads/ 与 /library/ 是从 th_files 表以指针形式镜像过来的,没有任何字节被复制进
PostgreSQL。解析出的文本会内联进来,好让 grep 能用;原始字节则在模型读该文件时以多模态
形式呈现。匿名调用获得的是内存版 StateBackend,因此什么都不会留下。
一轮 DeepAgent 被六个中间件包住,按施加顺序:
-
ToolFaultMiddleware最外层,所以一个抛异常的工具会被兜住,而不是把这一轮直接终结。 -
InvalidToolCallRepairMiddlewareJSON 未能解析成功的工具调用会被修复,而不是被丢弃。 -
ModelCallLimitMiddleware真正的每轮预算:MODEL_CALL_LIMIT次模型调用(默认 50),到了就优雅收尾。RECURSION_LIMIT只是 LangGraph 的原始上限,留作兜底。 -
CodeInterpreterMiddleware来自langchain-quickjs:一个持久的进程内 JavaScript REPL,以eval暴露出来。包缺失时该中间件带一条 warning 被跳过,这一轮就没有它。 -
SkillsMiddleware启动时把每个 skill 的 frontmatter 注入提示词,只在任务需要时才通过/skills/挂载点取全文SKILL.md。匿名会话没有可读的挂载点,因此跳过。 -
UniversalPromptCachingMiddleware(ttl="5m")最后一个,所以它的判断说了算。在支持缓存的 provider 上把提示词标记为可缓存,在不支持的上被忽略。
deepagents 本来会附带、但这里刻意不要的有两样:
- 没有
task子代理。 流式转发子代理会把所有事件压到它结束之后才吐,而且在这里task永远只是一个空转的自我克隆。关掉它是通过注册 harness profile,而不是通过DISALLOWED_TOOLS_DEEP。 - 没有
write_todos。deepagents0.7 把TodoListMiddleware从默认栈里去掉了, DeepAgent 也没有把它加回来。
delete 这个文件工具同样被按名字排除:PostgreSQL 文件系统后端没有实现它,因此该工具未
包含在工具面内。
BaseAgent
Section titled “BaseAgent”BaseAgent 没有 LangChain agent 循环、没有中间件、没有虚拟文件系统。它渲染一份 prompt,把 我们的 MCP server 交给 provider,然后把结果转发出去。tool loop 归 provider 所有。
/mcp 准入规则: BaseAgent 的 client 只收 Responses 风格的 API,因为它的全部意义就在于把我们的 MCP server 交给别人的 agent loop。只提供 Chat Completions 的 provider,改由 DeepAgent 的 LangChain 栈来消费:在这里再写一个本地 function-call 循环,只是重复劳动。
| Provider | 类 | 默认模型 | 服务端 MCP | 有状态 |
|---|---|---|---|---|
| OpenAI Responses | OpenAIResponsesClient | gpt-5-nano | 支持 | 是 |
| Gemini Interactions | GeminiClient | gemini-2.5-flash | 暂用本地 function 兜底 | 是 |
| DeepSeek Responses | DeepSeekResponsesClient | deepseek-v4-flash | 不支持,只有 function 与服务端 web_search | 否 |
| DashScope Responses | DashScopeClient | qwen3.5-flash | 不支持:它的 MCP 只接受 server_protocol: "sse",我们是 streamable HTTP | 是 |
选它之前还有两件事值得先知道:
- 它默认只保留最近五个用户轮次的历史(
user_message_threshold)。 - 它的 provider 条目不带
llm_type。client 类是由provider 键名的前缀决定的:gemini…、gpt…、deepseek…、qwen…/dashscope…。键名没有匹配上任何已知前缀,就悄悄地 拿不到 client,该 provider 也随之从/api/models消失。
同一个问题由不同 agent 应答时,产出的形式不同。
| DeepAgent | BaseAgent | |
|---|---|---|
| 由谁产出 | 模型在正文里直接写一段纯数据 JSON 的 vis-chart 围栏块 | 不产出 |
| 由谁渲染 | Web 客户端,用该块里的 JSON 渲染成可交互图 | 消费它的那个 MCP 客户端自带的可视化 |
| 服务端往返 | 没有:不调工具,也不生成 PNG | 不适用 |
这些工具本身见内置工具。
配置 provider
Section titled “配置 provider”每个 agent 读自己那个 PROVIDERS_ 键,后缀是 agent 名字的大写形式:
PROVIDERS_DEEP: gemini-3.5-flash: llm_type: google-genai api_key: GOOGLE_API_KEY model: gemini-3.5-flash temperature: 1.0 claude-sonnet: llm_type: openai api_key: OPENROUTER_API_KEY base_url: https://openrouter.ai/api/v1 model: anthropic/claude-sonnet-4.6 temperature: 0.1 qwen-vl: llm_type: openai api_key: DASHSCOPE_API_KEY base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 model: qwen-vl-max supports_pdf: true supports_image: true每个条目的键就是客户端要请求的 provider 名。条目内部:
llm_type string default: openai
要初始化哪个 LangChain 集成:google-genai 或 openai。凡是通过 OpenAI 兼容端点访问的
(OpenRouter、DashScope、火山引擎)都是 openai 再加一个 base_url。
api_key string
是保存密钥的那个键的名字,不是密钥本身。 见下面的警告。
base_url string
OpenAI 兼容 provider 的端点。直接写字面 URL 可以;写一个能在环境变量或配置里解析成 URL 的 名字也接受。
model string required
上游的 model id。没有它的条目会在启动时带一条 warning 被跳过。
temperature number
原样透传,此处没列出的其它键也一样:include_thoughts、thinking_level 之类会不加改动
地送进 LangChain 的构造函数。
supports_pdf boolean
只在 OpenAI 兼容端点上需要声明,因为引擎推断不出它们的能力。true 表示以原生文件块发送
PDF;不设则以预先抽取的文本提供。
supports_image boolean
同上,用于图片。
- api_key: GOOGLE_API_KEY
- $GOOGLE_API_KEY,否则查配置键 GOOGLE_API_KEY
- api_key: sk-abc123…
- 被当成名字去查,什么也没查到
Prompt
Section titled “Prompt”PROMPTS_{NAME} 是一串 Jinja2 模板路径。每一项都可以用 路径@键名 后缀钉住该模板注册时用
的键名;不写后缀,键名就是文件名去掉 .jinja:
PROMPTS_DEEP: - agent/prompts/deep.jinjaDeepAgent 按请求里带的 prompt_name 选模板,选不到就回退到配置里的第一个。通过用户 prompt
接口存下的用户级 prompt 优先于两者。
和别处一样的那两个键,后缀是 agent 名字的大写形式:
| 配置键 | 作用 |
|---|---|
ALLOWED_TOOLS_{NAME} | 白名单:一旦设置,就只提供这些工具 |
DISALLOWED_TOOLS_{NAME} | 黑名单:在白名单之后生效,因此总是它说了算 |
发行版 config.yaml 把它们统统留空,也就是「全部被发现的工具」。它另外还带着
ALLOWED_TOOLS_RTC / DISALLOWED_TOOLS_RTC / PROMPTS_RTC 这样的占位项,那是一个已被删除
的 agent 留下的空键:无害,也提醒你那个后缀不过是个 agent 名字。
写你自己的 agent
Section titled “写你自己的 agent”AGENT_DIRS 的扫描方式和 MCP_TOOL_DIRS 一样,默认只有一条:包内的
mirobody/agent。把你自己的目录加上,并列在最前面:
AGENT_DIRS: - agents # 你的,先扫 - mirobody/agent # 包内默认约定很小:一个 agent 一个类,一个叫 generate_response 的异步生成器;如果这个
agent 带 provider,还要有一个把它的 PROVIDERS_ 条目变成 client 的 load_llm_clients。
继承 DeepAgent 就能把两者一起继承下来。
from typing import Any, AsyncGenerator
from mirobody.agent.deep_agent import DeepAgent
class TriageAgent(DeepAgent): """Registers as agent name "Triage"; configured with PROVIDERS_TRIAGE, ALLOWED_TOOLS_TRIAGE, DISALLOWED_TOOLS_TRIAGE and PROMPTS_TRIAGE."""
async def generate_response( self, user_id: str, messages: list[Any], **kwargs ) -> AsyncGenerator[dict[str, Any], None]: async for event in super().generate_response(user_id, messages, **kwargs): yield eventagent、工具、skill 与 /mcp 怎么拼在一起
每个 agent 都能调的那些,逐个参数说明
不写代码,也能教 agent 一套做法
provider 键、prompt,以及三层配置