Runlet:一个小而清晰、Provider Neutral 的 Python Agent Runtime
调用模型很简单,真正麻烦的是把它做成一个稳定、可控、可观测的
runtime。
大多数 Python 开发者都可以在几分钟内完成第一个 demo:发一个请求,
拿到模型回复,打印结果。真正的问题通常从下一步才开始。当一个简单的
模型调用逐渐演变成真实应用里的 agent runtime,你很快会碰到流式输出、
工具调用、多 Provider 差异、上下文控制,以及线上出了问题之后到底发生了
什么这些事情。
Runlet 就是在这样的背景下做出来的。
Runlet 是一个轻量、provider neutral 的 Python agent runtime library。
它不是托管平台,不是工作流引擎,也不是一整套应用框架。它更像一层专注
的运行时边界,给那些希望在自己 Python 应用里明确掌控 agent 执行过程的
开发者使用。
真正麻烦的,通常不是第一个 demo
第一个 demo 一般都不难。
难的是后面的事情:
- 你想同时支持多个模型提供方
- 你需要在同一条执行链路里处理 streaming 和 tool calling
- 你希望在模型调用前统一处理 request shaping 和 context control
- 你需要结构化事件来支撑调试、日志和 observability
- 你需要 state,但又不希望 runtime 库悄悄膨胀成一个 memory framework
很多 Python agent 方案都很擅长帮助你快速起步。但一旦你开始认真看
runtime 这一层,取舍就会变得很明显。有些方案和某一家 provider 的 API
耦合得很深;有些方案编排能力很强,但对想嵌入现有应用的团队来说太重;
有些方案让 happy path 看起来很顺,但 stream、tool loop 和 observability
最后还是散落在应用代码里。
我们想要的是一个更小、更清楚的答案。
我们反复遇到的四个 runtime 问题
1. Provider 差异会慢慢渗透到整个应用里
Provider 耦合通常不会只停留在网络客户端这一层。它会继续出现在消息格式、
tool call payload、stream 事件、reasoning / thinking 字段、request
options,甚至调用代码本身的形状里。一旦发生这种事,应用的其他部分也会
开始继承 provider 特有的假设。
在一次性原型里,这还算能忍。到了稍微长寿一点的系统里,这件事就会变得
越来越别扭。
Runlet 的思路是把 provider 视为 adapter,而不是把某个 provider 的 API
当成整个 runtime 的事实标准。核心 runtime 只处理自己的消息、请求、响应、
事件和工具调用契约。具体 provider 再去完成 Runlet runtime model 与 SDK
之间的翻译。
这听起来不复杂,但它很重要。它意味着即便不同 provider 暴露出来的 API
风格差异很大,runtime 本身依然可以保持稳定。
2. Streaming 和 tool execution 真正难的是运行时控制流
流式输出文本很容易演示。流式执行一个 agent,并不容易。
一旦工具进入链路,模型的流式输出就不再只是文本。它可能包含部分文本、
部分工具参数、一个完整的 tool call、工具执行结果,然后再进入下一轮模型
调用。直到最后拿到终态回复之前,这其实是一段持续演化的运行时控制流。
这时候应用代码很容易开始长出一堆临时逻辑:
- 读取 delta
- 识别是不是 tool call
- 拼装参数
- 执行工具
- 把工具结果补回上下文
- 再次调用模型
- 持续循环,直到真正结束
Runlet 把这段 streaming tool execution loop 收回到 runtime 内部。Provider
负责发出 provider-neutral 的 stream event,runtime 负责执行工具、追加
tool result,并继续推进下一轮模型调用,直到整个 run 完成。
这类循环不应该在每个应用里都重新写一遍。
3. Context control 很容易散落到应用各层
很多系统一开始都会把 context control 当成“后面再处理”的问题。结果到了
后面,它往往会演变成这样:
- prompt building 在一个地方
- request trimming 在另一个地方
- provider-specific options 又在别的层
- 处理 context overflow 的紧急修补散落在各个 handler 里
Runlet 把 context preparation 当成 runtime execution 的一部分,而不是一个
可有可无的辅助函数。模型请求在真正发出去之前,会先经过 runtime 的准备
流程。目标不是把所有策略都做成统一标准,而是先把 runtime 边界明确下来。
我们也为 provider-specific request settings 选了一个比较克制的扩展点:ModelRequest.options[...]。
这意味着连接级别的配置,比如 api_key、base_url、自定义 SDK client,
仍然放在 provider 构造参数里;而请求级别的行为则留在 request 对象上。这
是一个很小的设计决定,但随着 provider 增加,它能让 API 保持整洁。
4. Observability 往往加得太晚
即便系统很小,agent runtime 也已经不太容易调试了。如果没有结构化事件,
事情只会更快变糟。
你通常会想知道:
- 一次 run 是什么时候开始的
- 最终实际送去给模型的输入是什么
- context preparation 有没有修改请求
- 模型是什么时候被调用的
- 哪个工具被调用了
- 工具返回了什么
- 这次 run 是不是因为某个策略而提前停止
如果这些可见性不是从 runtime 设计阶段就带进去,团队最后往往只能靠日志、
tracing wrapper 或特殊分支补丁把它拼出来。
Runlet 采用的是 event-first 的思路。Run、模型调用、streaming delta、
tool execution、完成事件,都会发出结构化事件。Hooks 用来扩展行为,
Observers 用来看见发生了什么。把这两类职责分开,有助于避免 observability
反过来变成隐式控制流。
Runlet 的 runtime 边界大致是这样:

Runlet 想做得不一样的地方
Runlet 是刻意保持“小”的。
我们的目标不是去和所有 agent framework、orchestration engine 或托管平台
竞争。Runlet 想提供的是一层容易嵌入、行为明确、边界克制的 runtime layer。
到目前为止,这意味着:
- 一个 provider-neutral 的 runtime loop
- streaming 支持
- 非流式和流式路径下都由 runtime 接管的 tool execution
- 结构化事件
- 模型调用和工具执行周围的 hook points
- 轻量的 state primitives
- request-level provider options
Runlet 提供 state store primitives,但没有直接内置一套 conversation memory
framework。这是有意为之。不同应用对 memory、retention、summarization、
session policy 的要求差别很大。相比过早塞进一套一刀切的 memory 方案,我们
更愿意先暴露干净的基础原语,把上层策略留在应用侧。
Runlet 现在已经能做什么
Runlet 还在 beta 阶段,但已经可以拿来做真实的集成工作。
当前内置 provider 包括:
OpenAIChatCompletionsProviderOpenAIResponsesProviderAnthropicMessagesProvider
当前 runtime 能力包括:
- 非流式 completion
- 流式文本输出
- tool calling
- streaming tool execution
- provider 支持时的 reasoning / thinking 输出
- 结构化 runtime events
- 轻量 state primitives
落到工程实践里,这意味着你已经可以用 Runlet 承接那一部分大多数项目迟早都
得自己实现的能力:围绕模型调用的 runtime 行为本身。
Runlet 不打算做什么
Runlet 不打算变成下面这些东西:
- 托管式 agent 平台
- Web 框架
- 图工作流引擎
- UI 或 trace viewer
- 完整 memory framework
如果你想要的是一个自带 orchestration、persistence、retrieval 和应用脚手架
的一体化系统,那 Runlet 刻意没有往那个方向走。
这是一种设计选择,不是功能列表还没补齐。
Runlet 适合谁
如果你符合下面这些情况,Runlet 会比较合适:
- 你在构建一个需要嵌入 agent runtime 的 Python 应用
- 你希望把 provider 选择控制在 adapter 边界之后
- 你要的是一层小而清晰的 runtime,而不是完整平台
- 你愿意自己掌控应用层的 state 和 memory 策略
如果你更期待下面这些东西,Runlet 可能就不是最合适的选择:
- 一个 batteries-included 的托管 agent 平台
- 一个 graph-first 的 orchestration 模型
- 内置好的产品级 memory 和 retrieval 抽象
一个最小示例
import asyncio import os from dotenv import load_dotenv from runlet import Agent, Runtime from runlet.providers import OpenAIChatCompletionsProvider async def main() -> None: load_dotenv() provider = OpenAIChatCompletionsProvider( model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"), api_key=os.environ["OPENAI_API_KEY"], base_url=os.environ.get("OPENAI_BASE_URL"), ) agent = Agent( name="assistant", instructions="Be concise and helpful.", model=provider, ) result = await Runtime().run(agent, "Introduce Runlet in one sentence.") print(result.output) asyncio.run(main())
这个例子的重点并不是“调用模型有多难”。重点在于:同一个 runtime 边界可以
自然扩展到 streaming、tools、provider-specific request options、
structured events,以及应用自己管理的 state,而不需要中途把运行时模型
推倒重来。
试试 Runlet
如果你需要的正是这样一种 runtime 形状,可以从这里开始:

浙公网安备 33010602011771号