跳到主要内容

两个主抽象:Agent 与 Task

30 秒导读: Upsonic 里你几乎永远只碰两个对象——Task(一次「要做的事」:问题描述、能用的工具、参考上下文、期望的返回格式)和 Agent(一个「会做事的人」:挂着模型、记忆、安全护栏、可靠性层)。写好这两个,调一句 agent.do(task),框架就替你把中间那条又长又脏的管线跑完。本章讲清这两个对象各装了什么、do 一族入口分别在什么场景用,以及 __init__.py 暴露的那一排「运行器」(Agent / Team / Graph / Chat …)各自的定位。

本章是「地基」。管线内部每一步怎么跑,交给 第 2 章;模型、工具、安全各自成章(03/04/05)。读完本章,你应该能回答:「我 new 一个 Agent、new 一个 Task、调 do 会发生什么。」


1. 先建立心智模型:名词 vs 动词

Upsonic 的顶层设计可以用一句话概括:把「做什么」和「谁来做」彻底分开。

  • Task = 名词(要做的事)。 它是一个纯数据对象——描述、工具清单、上下文、期望返回格式。它自己不会跑,不知道用哪个模型。
  • Agent = 执行者(会做事的人)。 它挂着模型、记忆、安全策略、可靠性层。它知道怎么把一个 Task 跑完

这种拆分带来一个直接好处:同一个 Task 可以丢给不同 Agent 跑,同一个 Agent 可以复用于很多 Task。 就像「工单」和「工人」分开——工单描述活儿,工人带着工具来干。

最小的一次使用长这样(源码 docstring 里的例子,agent/agent.py:167-175):

from upsonic import Agent, Task

agent = Agent("openai/gpt-4o") # 执行者:只给了模型
task = Task("What is 1 + 1?") # 要做的事:只给了一句描述
result = agent.do(task) # 把 task 喂给 agent 跑

AgentTask 都从顶层包直接导出——但注意,upsonic/__init__.py 用的是懒加载:from upsonic import Agent 触发 __getattr___get_Agent() 才真正 import 重模块(__init__.py:59-67, 105-136)。这是为了让 import upsonic 本身很快,不被一堆重依赖拖慢。


2. Task:一次「要做的事」

Task 是个 Pydantic 模型(tasks/tasks.py:21 class Task(BaseModel))。字段很多,但真正你会主动传的就那么几个。先看这几个核心的。

2.1 你最常传的四个字段

字段类型作用源码
descriptionstr(必填)这次要做什么,自然语言tasks/tasks.py:26
toolslist[Any]这个 task 允许用的工具(函数 / ToolKit / MCP / 甚至别的 Agent)tasks/tasks.py:28
contextAny参考材料:字符串、别的 Task、KnowledgeBase、文件/文件夹路径tasks/tasks.py:33
response_formattype期望返回的结构;默认 str,可传一个 Pydantic 模型强制结构化输出tasks/tasks.py:30

其余大量字段(缓存、策略作用域、向量检索参数、usage/run 状态…)大多有默认值,进阶才碰。下面挑最能体现设计意图的三处细讲。

2.2 response_format:想要结构化输出就传个模型

默认 response_format = str,即返回纯文本。想让模型吐结构化结果,传一个 Pydantic 类即可:

# 示意,非源码
from pydantic import BaseModel

class Invoice(BaseModel): # 定义你想要的形状
tax_number: str
total: float

task = Task(
"从这张发票里读出税号和总额",
context=["invoice.pdf"], # 文件路径直接塞进 context(见 2.3)
response_format=Invoice, # 要求返回 Invoice 而不是一段文本
)

response_format 允许 Pydantic 模型 / str / None 三种(tasks/tasks.py:30Union[Type[BaseModel], type[str], None])。它怎么被翻译成模型端的「结构化输出」约束,是模型层的事,见 03-model-layer.md

2.3 精华:context 里的文件路径会被自动抽成附件

这是 Task 一个很贴心的设计。你往 context 里塞的如果是一个存在的文件路径(甚至文件夹),Task 会在构造时把它从 context 里摘出来、转成 attachment,而不是把这串路径当普通文本喂给模型。

流程发生在 __init__ 里(tasks/tasks.py:365-374):

# tasks/tasks.py:365-374 (节选)
context, extracted_files = self._extract_files_from_context(context)
...
if extracted_files:
attachments.extend(extracted_files)

真正干活的是静态方法 _extract_files_from_context(tasks/tasks.py:211)。它的判定逻辑值得一看:

  • 遍历 context(支持 list / dict / 单个字符串三种形态,tasks/tasks.py:235-301);
  • 每一项用 _is_file_path(tasks/tasks.py:104)判断是不是存在的文件——是,就摘去 extracted_files;
  • _is_folder_path(tasks/tasks.py:145)判断是不是文件夹——是,就用 _get_files_from_folder(tasks/tasks.py:184)递归把夹子里所有文件都抽出来;
  • 其余的才留在「清洗后的 context」里,当作真正的文本上下文。

一句话看这张「谁去哪」的分流图:

context 里的每一项


是存在的文件? ──是──▶ 移入 attachments(不再当文本)
│否

是存在的文件夹? ─是──▶ 递归取夹内所有文件 ──▶ attachments
│否

留在 cleaned_context(当文本上下文喂模型)

坑点(诚实提醒): 判定不只看「存不存在」。如果一个字符串长得像文件路径(以 .pdf/.md/.py 等结尾,或含 /\)但文件其实不存在,_is_file_path直接抛 FileNotFoundError(tasks/tasks.py:132-136),再被 __init__ 包成一条更友好的错误(tasks/tasks.py:367-368)。也就是说:往 context 塞一个拼错的 .pdf 路径,Task(...) 构造当场就炸,而不是静默当文本。

抽出来的文件后续怎么用?attachments 会被 attachments_base64 属性(tasks/tasks.py:696)逐个读成 base64,交给多模态模型。

2.4 Task 也承载「运行状态」

Task 不只是输入,它还是这次运行的状态载体——一次运行的结果和过程都记回同一个 task 对象上:

你能读到什么属性源码
最终输出task.responsetasks/tasks.py:741
这次跑的用量(token/成本/耗时)task.usagetasks/tasks.py:432
命中缓存了吗task.cache_hittasks/tasks.py:756
期间调了哪些工具task.tool_callstasks/tasks.py:766
运行状态(完成/暂停/取消/出错)task.statustasks/tasks.py:48
是否需要「续跑」而非重跑task.is_problematictasks/tasks.py:452

关键理解:「状态」和「配置」是分开管理的。 每次全新运行,管线第 0 步会调 task_start()(tasks/tasks.py:863),它先 reset_run_state()(tasks/tasks.py:820)把上一次的 responsetool_calls、缓存命中、策略标记等逐一清空,再新建 TaskUsage 开计时。但它绝不动你传的配置(description / tools / response_format)和持久字段(task_id_ / task_usage_id_)。这就是为什么同一个 task 对象可以安全地被重试、被续跑。

is_problematic(tasks/tasks.py:452)是这套状态机的关键判据:当 status 落在 paused / cancelled / error 三者之一,它返回 True,提示调用方——这次别用 do_async() 重头跑,要用 continue_run_async() 续跑(见 §4.3)。


3. Agent:一个「会做事的执行者」

Agent(agent/agent.py:150 class Agent(BaseAgent))是整个框架的集成中枢——模型、工具、记忆、上下文管理、缓存、安全策略、可靠性层、Canvas、可观测性,全挂在它身上。它的 __init__50+ 个参数(agent/agent.py:228-303),吓人,但绝大多数有默认值,Agent("openai/gpt-4o") 就能跑。

3.1 关键构造参数(按「关注点」分组)

下面把 __init__ 里最值得知道的参数按用途归类。完整清单看 agent/agent.py:228-303

模型 / 身份

参数默认作用
model"openai/gpt-4o"provider/model 字符串或 Model 实例;__init__ 里立刻 infer_model(model) 解析成模型对象(agent/agent.py:385-387)
name / role / goal / instructionsNoneAgent 的身份与职责,会拼进 system prompt

记忆 / 存储

参数默认作用
memoryNone对话历史 / 用户画像的记忆实例
dbNone数据库实例;若给了 db,它的 db.memory 会覆盖 memory(agent/agent.py:437-440)
session_id / user_idNone会话与用户标识,记忆按此分桶

安全护栏(见 05-safety-engine.md)

参数作用
user_policy / agent_policy输入 / 对输出的安全策略;各自被包进一个 PolicyManager(agent/agent.py:508-522)
tool_policy_pre / tool_policy_post工具执行前 / 后的策略,包进 ToolPolicyManager(agent/agent.py:534-537)
*_policy_feedback命中策略时是「直接拦」还是「带反馈重试」

可靠性 / 反思 / 上下文治理

参数默认作用
reliability_layerNone可靠性层(校验 agent / 编辑 agent / 多轮质量改进)
reflection / reflection_configFalse自我反思;reflection=True 但没给 config 时自动建默认 config(agent/agent.py:544-554)
context_managementFalse开启后上下文接近模型上限时自动裁剪/摘要历史;构造时就装好中间件(agent/agent.py:451-463)

工具 / 技能(见 04-tools-system.md)

参数作用
toolsagent 级工具(函数 / ToolKit / 别的 agent);构造末尾 _register_agent_tools() 立即注册(agent/agent.py:576)
skills技能包;其 get_tools() 会被并进 tools(agent/agent.py:484-485)
enable_thinking_tool / enable_reasoning_tool开启编排式思考 / 推理工具

运行行为

参数默认作用
retry1重试次数;< 1 直接抛 ValueError(agent/agent.py:467-468)
mode"raise"失败时抛异常还是返回 False;非法值抛错(agent/agent.py:469-470)
tool_call_limit100单次执行最多调多少次工具
printNone是否打印执行面板;和环境变量 UPSONIC_AGENT_PRINT 有优先级关系(agent/agent.py:431-433)

3.2 BaseAgent:一个空基类,专为打破循环依赖

Agent 继承自 BaseAgent,而 BaseAgent(agent/base.py:3)是个完全空的抽象类——pass 而已。

它存在的唯一理由写在 docstring 里:打破循环依赖。像 Graph 这种需要「持有某种 agent」的复杂组件,只要依赖这个轻量基类,就不必 import 沉重的 Agent 具体实现,从而避免 import 环。任何继承 BaseAgent 的类都能被塞进 Graph 当节点用。这是一个很典型的「用空基类当接口锚点」的工程手法。

3.3 构造时都发生了什么(不进管线)

Agent.__init__纯装配,不跑任何任务。按顺序做的关键几件事:

infer_model(model) # 解析模型 agent.py:385-387
装可观测性 / promptlayer agent.py:389-391
db 优先于 memory agent.py:437-440
context_management 中间件(若开) agent.py:451-463
校验 retry / mode 合法 agent.py:467-470
skills.get_tools() 并入 tools agent.py:484-485
建 PolicyManager × 2 + ToolPolicyManager × 2 agent.py:508-537
reflection 默认 config / processor agent.py:544-554
建 CacheManager + ToolManager,注册 agent 级工具 agent.py:563-576

理解这一点很重要:Agent 一 new 出来就已经把所有子系统装好了,do 只是「按下开始」。agent_id / agent_usage_id 则是懒生成——第一次访问才 uuid4() / new_usage_id(agent/agent.py:776-792)。


4. 入口方法族:do 一家子

Agent 对外的「跑一个 task」入口有好几个,但它们全都收敛到一个真正的核心:do_async(agent/agent.py:3649)。其余都是它的包装。

4.1 全景:谁调谁

do(task) ──────┐ 同步阻塞、不打印

print_do(task) ─┤──▶ _run_in_bg_loop(...) ──▶ do_async(...) ◀── 唯一核心
│ (丢进常驻后台事件循环) │
print_do_async ────┘ ▼
跑 24 步管线 PipelineManager
stream(task) ───▶ 起线程跑 astream, (见 02-execution-pipeline.md)
用队列把结果搬回同步侧
astream(task) ──▶ 独立的流式管线(不走 do_async)

关键点:同步方法不是自己实现的,而是把协程丢进一个进程级常驻的后台事件循环 _run_in_bg_loop(agent/agent.py:37-55)。为什么不用 asyncio.run()?docstring 说得很直白(agent/agent.py:14-18):asyncio.run() 每次会关掉事件循环,导致 OpenAI SDK 里缓存的 httpx.AsyncClient 连接失效;所以 Upsonic 维护一个常驻 loop,整个进程复用。

4.2 每个入口什么时候用

方法同步/异步打印面板典型场景源码
do同步否(除非配置开)脚本里最常用,拿返回值agent/agent.py:4074
do_async异步异步应用 / 并发跑多个agent/agent.py:3649
print_do同步交互演示,想看指标面板agent/agent.py:4129
print_do_async异步异步版演示agent/agent.py:4176
stream同步边生成边打印文本(同步侧)agent/agent.py:4267
astream异步边生成边消费(异步流)agent/agent.py:4341
continue_run同步HITL / 出错 / 取消后续跑agent/agent.py:4946

几个共同点,记住能省很多困惑:

  • 入参可以是 strTask、或它们的列表。str 时内部 _convert_to_task 直接包成 Task(description=...)(agent/agent.py:949-963);传多元素列表则逐个顺序执行、返回结果列表(do_async 开头的 _handle_task_list_async,agent/agent.py:3707)。
  • return_output=False(默认)返回内容本身(str / Pydantic 实例);return_output=True 返回完整的 AgentRunOutput(agent/agent.py:3677)。
  • print 的优先级:环境变量 UPSONIC_AGENT_PRINT > Agent 构造参数 print > 方法默认(do=不打印,print_do=打印),逻辑集中在 agent/agent.py:431-433_print_method_default 的传递。

4.3 continue_run:HITL 与错误恢复的「续跑」

普通的 do 是「从头跑到尾」。但 Upsonic 支持跑到一半停下来等人——这就是 HITL(Human-in-the-Loop,人在环路)。continue_run(agent/agent.py:4946)就是从暂停点接着往下跑,而不是重来。

它覆盖的续跑场景(docstring agent/agent.py:4966-4972):

场景怎么触发续跑
外部工具执行把带上外部结果的 task 传回去
用户确认批准 / 拒绝某个工具调用
用户输入为工具调用补上字段值
持久化 / 错误恢复run_id,从存储里加载状态续跑
取消后恢复run_id 续跑

它和 do 的分工用 task.is_problematic(§2.4)判定:statuspaused/cancelled/error → 用 continue_run;否则用 do。底层它靠 run_id 把一次运行的状态串起来——do_async 在跑之前会 register_run(run_id) 并把 run_id 写回 task(agent/agent.py:3784-3804),暂停时不清 run_id(agent/agent.py:3864-3865),这样才能被续上。

注意流式模式不支持 HITL。 astream 的 docstring 明确写了(agent/agent.py:4355),遇到 problematic 任务时它只发一条 warning 就 return(agent/agent.py:4397-4402)——要 HITL 请走 do_async / continue_run

4.4 as_mcp:把这个 Agent 变成一台 MCP 服务器

as_mcp(agent/agent.py:4214)是个有意思的「反向」入口:它把当前 Agent 包装成一个 MCP(Model Context Protocol)服务器,对外暴露一个 do 工具,别的系统就能像调用任何 MCP 工具那样把任务丢给这个 agent。

# 示意,非源码
server = agent.as_mcp(name="我的翻译助手")
server.run() # 起 stdio / sse / http 服务,别人就能连过来用

实现上它用 fastmcp 建一个 FastMCP 实例,注册一个内部 do(task: str) 工具,函数体里转调 agent.print_do(task)(agent/agent.py:4257-4263)。fastmcp 没装时会给出友好的安装提示(agent/agent.py:4230-4238)。这条线让 Upsonic 的 agent 既能 MCP 工具(见 04),也能被当成 MCP 工具——双向打通。


5. 一排「运行器」:__init__.py 暴露了什么

顶层 __init__.py__all__(__init__.py:145-159)导出了不止 Agent。它其实提供了一族不同抽象层级的运行器,对应不同复杂度的编排需求。选哪个,取决于你要跑的是「一个 agent」「一队 agent」还是「一张流程图」。

运行器一句话定位何时选它源码锚点
Agent(别名 Clanker)全功能单 agent,一切的中心绝大多数场景agent/agent.py:150;Clanker = Agentagent/agent.py:5424
AutonomousAgentAgent 子类,预装文件系统 + shell + 沙箱工作区编码助手 / DevOps / 需要读写文件和跑命令agent/autonomous_agent/autonomous_agent.py:30
Team多 agent 协作,支持嵌套团队分层多智能体、任务分派team/team.py:20
Graph用「节点 + 状态」把多个 task/agent 编成工作流,支持并行有明确 DAG / 条件流转的流程graph/graph.py:392
Chat有状态的会话编排器,绑存储、管记忆与成本多轮对话式应用chat/chat.py:40
Direct极简高速接口,无记忆 / 无 KB / 无工具,只求快纯抽取 / 一次性结构化调用direct.py:14
RalphLoop自主开发循环:无限轮里每轮起全新 Agent 跑一个 task长时自主编码(见 06)ralph/loop.py:24
Simulation场景模拟评测 / 演练simulation/simulation.py

怎么理解这张表?按「抽象层级从低到高」排:

Direct 只想要一次快速 LLM 调用,不要花活

Agent 一个完整 agent(工具/记忆/安全/可靠性) ← 本章主角

AutonomousAgent Agent + 现成的文件/shell 能力

Chat 把 Agent 包成有状态的多轮会话

Team / Graph 多个 agent / task 的编排(队列式 / 图式)

RalphLoop 用「每轮全新 Agent」堆出长时自主开发

Direct vs Agent 的取舍很能说明设计意图:Direct 的 docstring 直说它「focuses on maximum speed and direct data retrieval」,刻意砍掉记忆、KB、工具(direct.py:14-19)——当你只想「读个 PDF 返回一个字段」时,Agent 那套管线是浪费,Direct 才对。

AutonomousAgent 则是「加料的 Agent」:它继承 Agent(autonomous_agent.py:30),默认塞好 InMemoryStorage + Memory + 文件工具 + shell 工具 + 工作区沙箱,并把 print 默认改成 True(autonomous_agent.py:34-46)。想要一个「开箱能读写代码」的 agent,直接用它,别自己攒。


6. 边界与局限(诚实说明)

  • Task 的文件判定会「猜」路径:字符串以已知扩展名结尾、或含 /`` 却不存在时会抛 FileNotFoundError(tasks/tasks.py:132-136)。好处是早失败,代价是——一段碰巧长得像路径的普通文本可能被误判。这是刻意的严格取舍。
  • 流式不支持 HITL:astream 遇到需要人类介入的任务只会 warning 后 return(agent/agent.py:4397-4402)。
  • 同步入口依赖常驻后台 loop:do / print_do / stream 都把活儿丢给进程级的 _bg_loop(agent/agent.py:17-34)。这是为连接复用做的取舍,但也意味着有一个常驻守护线程。
  • 本章不覆盖管线内部:do_async 之后那 24 步(InitializationStepCallManagementStep,agent/agent.py:4650-4675)怎么跑,是 第 2 章 的事。

7. 代码地图(导航索引)

按「符号名」定位比行号更抗漂移(上游更新后行号会变,符号名通常还在)。

主题文件路径符号名
Task 数据模型src/upsonic/tasks/tasks.pyTask
context 里抽文件src/upsonic/tasks/tasks.pyTask._extract_files_from_context / _is_file_path / _is_folder_path / _get_files_from_folder
每次运行清状态src/upsonic/tasks/tasks.pyTask.reset_run_state / Task.task_start
需不需要续跑的判据src/upsonic/tasks/tasks.pyTask.is_problematic
Agent 集成中枢src/upsonic/agent/agent.pyAgent.__init__
空基类(破循环依赖)src/upsonic/agent/base.pyBaseAgent
核心异步入口src/upsonic/agent/agent.pyAgent.do_async
同步入口src/upsonic/agent/agent.pyAgent.do / Agent.print_do
常驻后台事件循环src/upsonic/agent/agent.py_get_bg_loop / _run_in_bg_loop
流式入口src/upsonic/agent/agent.pyAgent.stream / Agent.astream
HITL 续跑src/upsonic/agent/agent.pyAgent.continue_run / Agent.continue_run_async
暴露为 MCP 服务器src/upsonic/agent/agent.pyAgent.as_mcp
24 步管线清单src/upsonic/agent/agent.pyAgent._create_direct_pipeline_steps
顶层导出与懒加载src/upsonic/__init__.py__getattr__ / _get_Agent / __all__
加料 agentsrc/upsonic/agent/autonomous_agent/autonomous_agent.pyAutonomousAgent
极简高速接口src/upsonic/direct.pyDirect
多 agent / 工作流 / 会话src/upsonic/team/team.pygraph/graph.pychat/chat.pyTeam / Graph / Chat
自主开发循环src/upsonic/ralph/loop.pyRalphLoop

下一步: 想知道 agent.do(task) 之后那条管线逐步发生了什么(记忆加载 → 建 prompt → 装工具 → 调模型 → 处理响应 → 存记忆…),读 02-execution-pipeline.md