DSPy 完整移植到 BEAM 的 Imp 项目解析

Imp 是一个把 DSPy 整套概念移植到 BEAM 虚拟机的 Elixir 库,仓库自述 full port of DSPy to the BEAM。它保留了 DSPy 的声明式写法,签名、模块、优化器、评估一整套名字都在,运行时换成 OTP 的进程模型,一个 agent 跑成一个受监督的进程。当前版本 0.5.0,MIT 许可证,要求 Elixir 1.19 以上,README 明确说这一版是实验性质,是它在 Hex 上的第一次发布。迁移文档里写明对应 DSPy 3.3.1。这个项目值得关注的地方不在又少一个 LLM 框架,而在它把 Python 生态里靠约定维持的那些约束换成了 Erlang 虚拟机原生的能力。

移植覆盖了哪些概念

DSPy 的核心是把提示词当成程序来写。用签名描述输入输出,用模块组合逻辑,用优化器自动搜索更好的指令和少样本示例,最后用评估集量化效果。Imp 的迁移文档给了一张逐条对照表。

dspy.Signature 对应 Imp.signature,一行字符串描述输入输出,类型上支持数组、枚举和数字。dspy.Predict、dspy.ChainOfThought、dspy.ReAct 分别对应 Imp.predict、Imp.chain_of_thought、Imp.react。dspy.Tool 对应 Imp.tool,还能通过 Imp.MCP.connect 直接导入 MCP server 的工具。优化器这边的名字几乎原样保留,BootstrapFewShot、MIPROv2、SIMBA、GEPA 都在 Imp.Optimizer 命名空间下。

一个最小可用的例子长这样。

lm = Imp.req_llm("openai:gpt-5.4-mini", api_key: System.fetch_env!("OPENAI_API_KEY"))

triage =
  "issue -> kind: enum[bug,feature,question], summary"
  |> Imp.signature("Triage a GitHub issue.")
  |> Imp.predict(lm: lm)

{:ok, prediction} = Imp.call(triage, %{issue: "App crashes on startup"})

LLM 客户端走 ReqLLM 这个库,它支持多家 provider,换模型只改模型标识字符串。

agent 就是一个进程

这是移植里最有意思的一层。DSPy 在 Python 里用协程和并发池模拟并发,Imp 直接说 on the BEAM, an agent is a process。

Imp.start_run 把一次程序执行跑成一个受监督的进程,可以超时等待,也可以取回完整的事件序列。

{:ok, run} =
  Imp.start_run(researcher, %{question: question},
    authorize: fn call ->
      url = call.arguments["url"] || ""
      if String.contains?(url, "raw.githubusercontent.com"),
        do: :allow,
        else: {:deny, :untrusted_host}
    end
  )

for event <- Imp.Run.events(run), do: event.kind

事件序列把一次运行摊开成 :run_started、:tools_sent、:model_request、:model_response、:tool_call、:tool_result、:run_finished。调试时可以直接对照是模型出问题还是工具出问题,不用在嵌套调用里插日志。

authorize 回调值得单独说。它可以拒绝某一次工具调用,上面这段代码让 agent 只能访问白名单域名,其他一律拒绝。给模型开放工具的同时保留一道闸门,这个模式在 Python 侧要么自己包,要么绕。

运行时借了虚拟机哪些能力

监督是执行模型本身。并行调用和评估行跑在一个大小有限的受监督任务池里,单个样本失败只变成一条 error row,不会让整个 run 崩掉。做优化器评估时这点很实际,一次跑几百个样本,偶发的解析失败不该让几十分钟的搜索白费。

返回值统一成 {:ok, prediction} 或 {:error, reason},解析失败、provider 失败、工具失败各有自己的形状。Erlang 的传统是不抛异常穿越进程边界,错误即值在这里变成了框架的默认约定。

超时可以硬性指定,Imp.Deadline.with_deadline 给整段执行设截止时间。文档也说明了一个缺口,工具函数本身没有超时,MCP 工具默认 30 秒,可能已经生效的工具调用会被报告为 unknown,不会被悄悄重试。

序列化也换掉了。Imp.save! 和 Imp.read! 把整个程序存成带校验和的 JSON,不是 Python 侧的 pickle。这让优化出来的程序可以被 diff、被版本控制、被别的语言读取。

目前的使用门槛

它要求 Elixir 1.19 以上,还要本机有 C 和 C++ 编译器,因为 jaxon 和 erlexec 两个依赖带原生代码,首次编译需要联网拉取 rebar3 插件。

  1. 5.0 是它在 Hex 上的第一次发布,文档里写明 API 可能还会变,优化器需要大规模基准测试。GRPO 那一项标了实验性,通过兼容 TRL 的训练 worker 实现。

它对应 DSPy 3.3.1,后续版本的新特性还在往过搬。已经熟悉 DSPy 的人迁移成本主要在语言,不在这套概念。

常见问题

比 Python 版快吗? 主要收益不是单次请求快。并发多个 agent、跑大规模评估、长时间不重启地驻留,这些场景才是进程模型的强项。

能在生产里用吗? 眼下适合评估和小规模试用。优化器的效果需要你自己在业务数据上测,项目方没有提供基准。

必须用 Elixir 吗? 是。这是给 BEAM 生态的移植,不是跨语言绑定。