rlm 开发与扩展指南:编写 LM 客户端、REPL 环境以及环境 ↔ LM Handler 的通信协议
2026/9/17 5:23:46 网站建设 项目流程

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(如LocalREPLPortkeyClient),常量用 UPPER_CASE(如_SAFE_BUILTINSRLM_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() 的实现可以看到当前的路由表:openaivllmportkeyopenroutervercelanthropicgeminiazure_openai。其中vllm复用OpenAIClient并断言必须传入base_url(本地 vLLM 服务地址);openroutervercel则分别用setdefault注入默认网关地址——这正是上文"硬编码合理默认值"规范的直接体现。

在运行时,客户端并非被主进程直接调用,而是被 LMHandler 包装成多线程 TCP 服务:get_client(model, depth) 的路由逻辑是——显式指定了已注册的model时优先按模型名取客户端(覆盖 depth 路由);depth=0走默认客户端(主 backend);depth=1other_backend_client(若存在),否则回落到默认客户端。批量请求则经由 LMRequestHandler._handle_batched() 用asyncio.gather并发执行、以Semaphore(batch_max_concurrent)(默认 16)限流,且单个 prompt 失败不会拖垮整个批次。

四、开发 REPL 环境

环境实现位于rlm/environments/,需要先选择正确的基类:

模式基类适用场景抽象方法
非隔离(Non-isolated)NonIsolatedEnv本机执行、与 RLM 同机setupload_contextexecute_code
隔离(Isolated)IsolatedEnv云沙箱(Modal、Prime 等)setupload_contextexecute_code

两个基类都继承自 BaseEnv,其构造函数默认参数为persistent=Falsedepth=1max_concurrent_subcalls=4

4.1 实现要求

  • 继承 rlm/environments/base_env.py 中的NonIsolatedEnvIsolatedEnv
  • 实现全部抽象方法setupload_contextexecute_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_queryllm_query_batchedrlm_queryrlm_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_queryrlm_querycontexthistoryanswerSHOW_VARS)在每次执行后被恢复。

当前get_environment()已支持localipythonmodaldockerdaytonaprimee2b七种环境;新环境加入后在此处分发即可被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为例):

  1. 代码执行期间环境内的llm_query(prompt)rlm_query(prompt)被调用;
  2. llm_query构造LMRequest并调用send_lm_request(address, request)rlm_query则通过subcall_fn派生一个子 RLM,达到最大深度时回落到llm_query
  3. 客户端向(host, port)建立新的 TCP 连接;
  4. 发送带长度前缀的 JSON 请求,LMHandler由 LMRequestHandler.handle() 处理(批量/单条分流、异常时以LMResponse.error_response回传而不是崩溃);
  5. 返回携带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与之通信。

工作流程

  1. 沙箱创建:环境创建云沙箱,并在沙箱内部启动一个 HTTP broker 服务;
  2. 隧道暴露:broker 通过供应商的加密隧道对外暴露(如 Modal 的encrypted_ports);
  3. 代码执行:沙箱内调用llm_query()时 POST 到http://localhost:8080/enqueue(沙箱内 broker 基地址形如http://127.0.0.1:{broker_port},见 BROKER_URL);
  4. 请求入队:broker 把请求排队并阻塞等待响应;
  5. 宿主轮询:宿主侧ModalREPL后台线程轮询{tunnel_url}/pending获取新请求(_poll_broker() 中轮询间隔为time.sleep(0.1),即 100ms);
  6. LM 转发:宿主把请求经 socket 转发给LMHandler并取得响应;
  7. 响应回传:宿主 POST 到{tunnel_url}/respond
  8. 解除阻塞:broker 用响应解除原始/enqueue调用的阻塞。

Broker 端点表(Flask 路由在 rlm/environments/modal_repl.py 中可逐一对应):

端点方法用途
/enqueuePOST沙箱代码提交 LLM 请求(阻塞直至响应)
/pendingGET获取待处理请求列表(由宿主轮询器调用)
/respondPOST为某个 request ID 提交响应(由宿主轮询器调用)
/healthGET健康检查

关键实现细节

  • broker 是运行在沙箱内部的 Flask 服务,使用threading.Event做请求/响应的同步(respond端点中entry["event"].set()即解除阻塞点);
  • 宿主侧 poller 线程在后台运行,轮询间隔 100ms;
  • 执行状态通过dill序列化持久化到/tmp/rlm_state.dill(STATE_FILE),在代码块之间做 state 的存取。

5.3 实现一个新的隔离环境

AGENTS.md 给出的五步清单(以新云厂商为例):

  1. 创建 broker 服务——实现/enqueue/pending/respond端点的 Flask/HTTP 服务;
  2. 暴露隧道——用供应商的隧道/端口转发把 broker 暴露给宿主;
  3. 实现 poller——宿主上的后台线程,负责轮询并转发请求;
  4. 编写执行脚本——在沙箱内运行、其中llm_query()调用 broker 的脚本;
  5. 处理状态——在代码块之间序列化/反序列化执行状态。

指南明确指定参考实现: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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询