rlm 开发与扩展指南:编写 LM 客户端、REPL 环境以及环境 ↔ LM Handler 的通信协议
【免费下载链接】rlmGeneral plug-and-play inference library for Recursive Language Models (RLMs), supporting various sandboxes.项目地址: https://gitcode.com/GitHub_Trending/rlm/rlm
本文基于 rlm 仓库根目录下的贡献者指南 AGENTS.md 展开,系统讲解递归语言模型(Recursive Language Models, RLM)推理库rlm的工程规范、两类扩展点(rlm/clients/中的 LM 客户端与rlm/environments/中的 REPL 环境)的完整实现模式,以及环境与 LM Handler 之间基于 TCP socket 与 HTTP Broker 的通信架构。读完后,你能够按仓库标准独立编写并注册新的模型客户端或沙箱环境,并理解llm_query()/rlm_query()在代码执行期间如何跨进程/跨机器路由回宿主机的 LM 服务。
一、开发环境搭建
仓库指南明确使用 uv 作为开发工具链,Python 版本建议 3.12(README 中声明运行时最低要求为 Python 3.11):
# 安装 uv(首次) curl -LsSf https://astral.sh/uv/install.sh | sh # 如需初始化空白项目 uv init && uv venv --python 3.12 source .venv/bin/activate # 以可编辑模式安装 uv pip install -e . # 启用 Modal 沙箱支持 uv pip install -e ".[modal]" # 启用 Prime 沙箱支持 uv pip install -e ".[prime]"核心开发工作流在 AGENTS.md 中给出的标准命令为:
# 常规开发同步 uv sync # 安装 dev + test 依赖组 uv sync --group dev --group test # 安装 pre-commit 钩子 uv run pre-commit install仓库同时附带 Makefile,提供make install(安装基础依赖)与make check(运行 linter、formatter 与测试)两个常用入口,与 AGENTS.md 的 PR 前检查清单相互呼应。
二、通用工程规范
2.1 代码风格与类型标注
AGENTS.md 对风格与类型化给出三条硬性约束:
- 格式化:严格使用
ruff,所有 PR 必须通过ruff check --fix .; - 类型标注:优先显式类型。可接受
cast(...)、assert ...做类型收窄;简单的参数场景(如 prompt 处理器)可以接受无类型参数;不接受没有充分理由的# type: ignore; - 命名约定:方法与变量用 snake_case,类用 PascalCase(如
LocalREPL、PortkeyClient),常量用 UPPER_CASE(如_SAFE_BUILTINS、RLM_SYSTEM_PROMPT);除非明确要求,不要给私有方法加_前缀。
2.2 错误处理哲学
指南的核心立场是"fail fast, fail loud"(快速失败、大声失败):
- 不做防御性编程,不做静默回退;
- 最小化分支:优先单一代码路径,每个
if/try都需要正当理由; - 典型例子:缺少 API key 时应立即抛出
ValueError,而不是优雅降级。
这一点在源码中可以得到印证:get_client() 遇到未知 backend 时直接raise ValueError;get_environment() 同样以ValueError拒绝未知环境名,二者都没有任何兜底路径。
2.3 依赖、测试、文档与改动范围
| 维度 | 要求 |
|---|---|
| 依赖 | 避免新增核心依赖;非必需功能走 optional extras(如modalextra);例外是"体积很小且能显著简化常用代码"的依赖 |
| 测试 | uv run pytest,用例放在tests/下;写简单、确定性的单元测试;功能变更必须同步更新测试;涉及隔离环境的测试要 mock 外部服务 |
| 文档 | 保持简洁可执行;行为变化时同步更新 README;避免内容重复 |
| 改动范围 | 小而聚焦的 diff,一个 PR 只做一件事;仅在不引入过多维护负担时才做向后兼容;删除死代码而不是保留保护逻辑 |
PR 提交前的完整检查清单:
# 风格 + lint 检查 uv run ruff check --fix . uv run ruff format . uv run pre-commit run --all-files # 运行测试 uv run pytest同时确保文档与测试已按需更新、死代码已删除,追求"最小外科手术式" diff。
三、开发 LM 客户端
LM 客户端实现位于rlm/clients/,所有客户端必须继承 BaseLM。
3.1 基类接口
BaseLM定义了四个必须实现的抽象方法(见 rlm/clients/base_lm.py):
| 抽象方法 | 职责 |
|---|---|
completion(prompt) | 同步单次补全,返回字符串 |
acompletion(prompt) | 异步单次补全(批量并发路径使用) |
get_usage_summary() | 返回全部调用的聚合用量(UsageSummary) |
get_last_usage() | 返回最近一次调用的模型级用量(ModelUsageSummary) |
基类构造函数签名提供了几个值得注意的默认行为:timeout默认 300 秒(模块级常量DEFAULT_TIMEOUT),sampling_args(temperature、top_p、max_tokens、seed 等)会作为**self.sampling_args转发给底层补全 API。
3.2 实现要求与结构示例
AGENTS.md 列出的硬性要求:
- 继承 rlm/clients/base_lm.py 中的
BaseLM; - 实现全部四个抽象方法;
- 按模型追踪用量(调用次数、输入/输出 token);
- 同时支持字符串与消息列表两种 prompt 形态;
- 在 rlm/clients/__init__.py 中注册新客户端。
仓库给出的标准骨架:
from rlm.clients.base_lm import BaseLM from rlm.core.types import ModelUsageSummary, UsageSummary class MyClient(BaseLM): def __init__(self, api_key: str, model_name: str, **kwargs): super().__init__(model_name=model_name, **kwargs) # 初始化你的客户端 def completion(self, prompt: str | list[dict[str, Any]], model: str | None = None) -> str: # 同时处理 str 与消息列表两种格式 # 用 _track_cost() 记录用量 # 返回响应字符串 def get_usage_summary(self) -> UsageSummary: # 返回跨全部调用的聚合用量3.3 配置规范
- 环境变量:只用于 API key(并在 README 中说明);
- 硬编码:默认 base URL 与合理默认值;
- 构造参数:必要的定制项通过
__init__()传入。
3.4 客户端如何被路由与调用
从 get_client() 的实现可以看到当前的路由表:openai、vllm、portkey、openrouter、vercel、anthropic、gemini、azure_openai。其中vllm复用OpenAIClient并断言必须传入base_url(本地 vLLM 服务地址);openrouter与vercel则分别用setdefault注入默认网关地址——这正是上文"硬编码合理默认值"规范的直接体现。
在运行时,客户端并非被主进程直接调用,而是被 LMHandler 包装成多线程 TCP 服务:get_client(model, depth) 的路由逻辑是——显式指定了已注册的model时优先按模型名取客户端(覆盖 depth 路由);depth=0走默认客户端(主 backend);depth=1走other_backend_client(若存在),否则回落到默认客户端。批量请求则经由 LMRequestHandler._handle_batched() 用asyncio.gather并发执行、以Semaphore(batch_max_concurrent)(默认 16)限流,且单个 prompt 失败不会拖垮整个批次。
四、开发 REPL 环境
环境实现位于rlm/environments/,需要先选择正确的基类:
| 模式 | 基类 | 适用场景 | 抽象方法 |
|---|---|---|---|
| 非隔离(Non-isolated) | NonIsolatedEnv | 本机执行、与 RLM 同机 | setup、load_context、execute_code |
| 隔离(Isolated) | IsolatedEnv | 云沙箱(Modal、Prime 等) | setup、load_context、execute_code |
两个基类都继承自 BaseEnv,其构造函数默认参数为persistent=False、depth=1、max_concurrent_subcalls=4。
4.1 实现要求
- 继承 rlm/environments/base_env.py 中的
NonIsolatedEnv或IsolatedEnv; - 实现全部抽象方法
setup、load_context、execute_code; execute_code()必须返回REPLResult(定义在 rlm/core/types.py);- 处理
lm_handler_address,使llm_query()与rlm_query()能跨进程调用 LM; - 实现
cleanup()做资源管理; - 在 rlm/environments/__init__.py 中注册环境。
关键实现细节的语义:
setup():初始化全局/局部命名空间与辅助函数;load_context():把上下文载荷作为context变量暴露给被执行的代码;execute_code():执行代码并捕获 stdout/stderr,返回REPLResult;- 环境全局中必须始终提供
llm_query、llm_query_batched、rlm_query、rlm_query_batched四个函数。
4.2 状态管理:执行代码可用的保留全局量
AGENTS.md 规定每个环境必须向被执行的代码提供以下全局变量,其中保留名集合在 RESERVED_TOOL_NAMES 中有精确对应:
| 全局名 | 语义 |
|---|---|
context | 已加载的上下文载荷 |
llm_query(prompt, model=None) | 纯单次 LM 补全(不进 REPL、不迭代) |
llm_query_batched(prompts, model=None) | 批量纯 LM 补全 |
rlm_query(prompt, model=None) | 递归子 RLM 调用(拥有独立 REPL 与迭代);达到最大深度时回落到llm_query |
rlm_query_batched(prompts, model=None) | 批量递归子 RLM 调用 |
answer | 字典{"content": "", "ready": False};模型写入answer["content"]并置answer["ready"] = True后,环境将内容挂到REPLResult.final_answer上 |
SHOW_VARS() | 列出当前可用变量 |
这些名字是保留的:自定义工具不允许覆盖它们,且每次代码执行结束后会被恢复,防止命名空间被污染。这一机制由 validate_custom_tools() 在入口处强制校验(冲突即抛ValueError)。
4.3 结构示例与环境检查清单
from rlm.environments.base_env import NonIsolatedEnv from rlm.core.types import REPLResult class MyEnvironment(NonIsolatedEnv): def __init__(self, lm_handler_address: tuple[str, int] | None = None, context_payload: dict | list | str | None = None, **kwargs): super().__init__(**kwargs) self.lm_handler_address = lm_handler_address self.setup() if context_payload: self.load_context(context_payload) def setup(self): # 初始化执行命名空间 def load_context(self, context_payload: dict | list | str): # 让 context 对执行代码可见 def execute_code(self, code: str) -> REPLResult: # 执行代码并返回 REPLResult def cleanup(self): # 清理资源交付前的环境检查清单(源自 AGENTS.md):
- 遵循上述指南;
- 环境能配合基础 RLM 补全调用正常工作;
cleanup()正确释放所有资源;- 子 LM 调用通过
llm_query()与rlm_query()可用; - 保留名(
llm_query、rlm_query、context、history、answer、SHOW_VARS)在每次执行后被恢复。
当前get_environment()已支持local、ipython、modal、docker、daytona、prime、e2b七种环境;新环境加入后在此处分发即可被RLM(environment=...)使用。
五、架构:环境 ↔ LM Handler 的通信
理解环境与 LM Handler 的通信方式是开发新环境的前提。AGENTS.md 给出了总体拓扑:宿主进程中的 RLM 主循环与LMHandler(一个ThreadingTCPServer)互连,非隔离环境(如LocalREPL)再通过同一 TCP socket 协议把llm_query()/rlm_query()转发给 LMHandler。
5.1 Socket 协议(非隔离环境)
协议格式:4 字节大端长度前缀 + UTF-8 JSON 载荷。发送侧实现见 socket_send():
def socket_send(sock: socket.socket, data: dict) -> None: payload = json.dumps(data).encode("utf-8") sock.sendall(struct.pack(">I", len(payload)) + payload)接收侧 socket_recv() 先读 4 字节长度,再按长度循环recv收满整个消息体;若在消息收全前连接断开,抛出ConnectionError。
请求流程(以llm_query为例):
- 代码执行期间环境内的
llm_query(prompt)或rlm_query(prompt)被调用; llm_query构造LMRequest并调用send_lm_request(address, request);rlm_query则通过subcall_fn派生一个子 RLM,达到最大深度时回落到llm_query;- 客户端向
(host, port)建立新的 TCP 连接; - 发送带长度前缀的 JSON 请求,
LMHandler由 LMRequestHandler.handle() 处理(批量/单条分流、异常时以LMResponse.error_response回传而不是崩溃); - 返回携带
RLMChatCompletion或 error 字段的LMResponse。
关键组件:
- LMHandler:多线程 TCP 服务器,包装 LM 客户端,支持上下文管理器(
start()/stop()); - LMRequest / LMResponse:类型化的请求/响应 dataclass,同时支持单条(
prompt)与批量(prompts)两种形态; - send_lm_request() / send_lm_request_batched():socket 通信的 typed 辅助函数,异常统一收敛为 error 响应而非抛出。
5.2 HTTP Broker 模式(隔离环境)
隔离环境(Modal、Prime)运行在云端机器上,无法直连宿主机的 socket 服务,因此采用 HTTP broker 中转。拓扑为:宿主机上的ModalREPL轮询器通过隧道访问沙箱内的 Flask broker;broker 内由执行脚本经localhost与之通信。
工作流程:
- 沙箱创建:环境创建云沙箱,并在沙箱内部启动一个 HTTP broker 服务;
- 隧道暴露:broker 通过供应商的加密隧道对外暴露(如 Modal 的
encrypted_ports); - 代码执行:沙箱内调用
llm_query()时 POST 到http://localhost:8080/enqueue(沙箱内 broker 基地址形如http://127.0.0.1:{broker_port},见 BROKER_URL); - 请求入队:broker 把请求排队并阻塞等待响应;
- 宿主轮询:宿主侧
ModalREPL后台线程轮询{tunnel_url}/pending获取新请求(_poll_broker() 中轮询间隔为time.sleep(0.1),即 100ms); - LM 转发:宿主把请求经 socket 转发给
LMHandler并取得响应; - 响应回传:宿主 POST 到
{tunnel_url}/respond; - 解除阻塞:broker 用响应解除原始
/enqueue调用的阻塞。
Broker 端点表(Flask 路由在 rlm/environments/modal_repl.py 中可逐一对应):
| 端点 | 方法 | 用途 |
|---|---|---|
/enqueue | POST | 沙箱代码提交 LLM 请求(阻塞直至响应) |
/pending | GET | 获取待处理请求列表(由宿主轮询器调用) |
/respond | POST | 为某个 request ID 提交响应(由宿主轮询器调用) |
/health | GET | 健康检查 |
关键实现细节:
- broker 是运行在沙箱内部的 Flask 服务,使用
threading.Event做请求/响应的同步(respond端点中entry["event"].set()即解除阻塞点); - 宿主侧 poller 线程在后台运行,轮询间隔 100ms;
- 执行状态通过
dill序列化持久化到/tmp/rlm_state.dill(STATE_FILE),在代码块之间做 state 的存取。
5.3 实现一个新的隔离环境
AGENTS.md 给出的五步清单(以新云厂商为例):
- 创建 broker 服务——实现
/enqueue、/pending、/respond端点的 Flask/HTTP 服务; - 暴露隧道——用供应商的隧道/端口转发把 broker 暴露给宿主;
- 实现 poller——宿主上的后台线程,负责轮询并转发请求;
- 编写执行脚本——在沙箱内运行、其中
llm_query()调用 broker 的脚本; - 处理状态——在代码块之间序列化/反序列化执行状态。
指南明确指定参考实现:rlm/environments/modal_repl.py 是隔离环境的 canonical reference,配合 tests/test_docker_repl_robustness.py、tests/test_local_repl_persistent.py 等测试用例可以对照验证行为。
六、小结
AGENTS.md 把 rlm 的扩展体系归纳为两条对称的路径:新增模型后端只需继承BaseLM并实现四个抽象方法、接入get_client()路由;新增执行环境则按隔离级别选择NonIsolatedEnv/IsolatedEnv,补齐setup/load_context/execute_code/cleanup与六个保留全局量,再接入get_environment()。而贯穿两类扩展的底层骨架,是"长度前缀 JSON over TCP"的 socket 协议与面向云沙箱的 HTTP broker 中转模式——前者由 rlm/core/comms_utils.py 与 rlm/core/lm_handler.py 实现,后者以 rlm/environments/modal_repl.py 为范本。遵循仓库"fail fast、最小分支、死代码即删"的工程哲学完成上述检查清单,即可交付一个符合仓库标准的扩展 PR。
【免费下载链接】rlmGeneral plug-and-play inference library for Recursive Language Models (RLMs), supporting various sandboxes.项目地址: https://gitcode.com/GitHub_Trending/rlm/rlm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考