llm 与 agent:编排层的两个原语

编排层写两种意图:要一段模型输出,和要一个自主干活的工作者。两种意图两个名字: llm() 是一次模型调用,agent() 是带工具循环的完整 turn。 agent 就是循环里的 llm:它的每一轮说话走的是同一个单次调用层。 runtime.exec 退回基础设施层(provider 传输、嵌入、测试),编排与业务代码不再直接使用它。

1. 两个原语,一个底座

编排代码(agentics / workflow / harness) 要一段输出 要一个工作者 llm(prompt, model=, effort=) 一次模型请求,返回文本(或结构化结果) prompt:str 或 content blocks(可带图) 不建 session 分支,不进工具循环 DAG:所在函数轨迹里的一次 ModelCall agent(prompt, description=, agent_id=) session 分支 + 资源限额,DAG:独立分支 循环体(agent = loop 里的 llm) 一次单次调用(说话) 执行工具,回填结果 要工具 就继续 一次请求 每轮一次请求 单次调用层(Runtime,基础设施,保留) provider 适配(Anthropic / Gemini / Codex …)· 认证 · 重试/超时/取消 · usage 统计 · 嵌入 Runtime(call=…) · 测试注入缝
两个原语共用一个单次调用层。llm 是一次请求;agent 是 session 分支里循环的 llm 加工具执行。 分流发生在建 session 分支之前:llm 的路径上根本不存在分支、transcript、工具循环这些设施。

2. llm():一次模型调用

reply = llm("把下面的段落压缩成三句话:\n" + text)

verdict = llm(
    [{"type": "text", "text": "这张截图里登录按钮在哪?给出坐标。"},
     {"type": "image", "source": …}],
    model="gemini:gemini-3-pro", effort="low")
方面定义
签名llm(prompt, *, model="", effort="", response_format=None, choices=None, web_search=False, timeout_s=None) -> str | dictprompt 收 str 或 content blocks (text/image 混排),str 在入口转成一个 text block。
语义恰好一次模型请求(传输层重试不算第二次)。没有 session 分支、没有 transcript、 没有工具循环。返回模型输出本身,不带 [spawned agent …] 尾巴。
覆盖优先级调用处参数 > 会话/runtime 默认。不给 model 就用当前会话的模型。
runtime 来源环境获取:agentic function 执行时框架已把当前 Runtime 放进 contextvar(_current_runtime),llm 从那里拿。函数签名不再串 runtime: Runtime 参数。
归属openprogram/agentic_programming/ 导出,和 agentic_function 同层。DAG 里就是现在的 ModelCall 节点。
实现薄封装:对环境 runtime 调 exec(content=…, tools=[], max_iterations=1) 一类的 单次形态。runtime.exec 的工具循环参数(tools/toolset/ max_iterations/tool_choice)不出现在 llm 上——要循环就用 agent。

3. agent():循环的 llm

公开形态不变:agent(prompt, description="", agent_id="", start_from="clean", run_in_background=False, …),模型与工具集来自 agent_id 选的 profile, 运行建 session 分支、受资源限额管、DAG 记独立分支(倒三角)。本次完善两件事:

完善内容
说话走单次调用层循环体内每轮的模型请求与 llm 走同一层, 共享认证、重试、usage 统计和测试注入缝。llm 与 agent 的差异只剩循环和 session,没有第二条请求通道。
可观测性分开计数每次调用记录 execution_kind = llm | agent、 provider 请求数、agent 迭代数。llm 的传输重试不计入迭代数,"一次调用"可验收。
不改的:agent 的多轮驱动力仍由工具循环决定,不加"最多 N 轮"参数;profile(AgentSpec)仍是模型与 工具集的载体。调研确认的风险记录在案:Codex/Claude Code 都存在无工具也继续的机制 (stop hook、pending input、compaction 后继续),OpenProgram 若将来引入这类 continuation, 它属于 agent 的循环层,不影响 llm"恰好一次请求"的定义。

4. 迁移:runtime.exec 全部改名

编排与业务代码里每个 runtime.exec(…) 按一条规则改写:

不带工具参数(绝大多数)→ llm(…);带 tools/toolset/ max_iterations>1(少数:wiki ingest、deep_work 的工作步、research 的 survey 步) → 保持工具循环语义,改写为 agent(…) 或暂以 llm 之外的既有路径运行,逐处判断。 函数签名里的 runtime: Runtime 参数随迁移删除,调用链同步收紧。
范围说明
1新增 llm()agentic_programming 导出)+ agent 说话改走单次调用层 + 计数含单测:一次请求、content blocks 原样进 provider、 model/effort 覆盖不改全局、无 session 分支产生。workflow 注入环境加入 llm
2本体编排代码:functions/agentics/(deep_work、ask_user、 extract_pdf_*、research/、示例)、memory/wiki/约 20 个文件;对应单测同批适配。
3嵌套 harness 仓库(functions/agentics/ 下):Research-Agent-Harness (约 80 处,stages 各目录)→ GUI-Agent-Harness(planning / benchmarks,截图调用靠 content blocks)→ Wiki-Agent-Harness逐仓一个子任务,嵌套仓库随改随同步。
4收尾编排层无 runtime.exec 残留;examples/ 与 provider/agent 内核明确保留 Runtime(嵌入与实现材料);产品文档与本页同步。

每批全绿才进下一批:批 2、3 是大面积机械替换,语义错误会成倍放大,串行是最便宜的隔离。

5. 调研依据

对 references/ 下八个框架的分层做了逐源码调研,支撑本设计的三条结论:

结论证据
成熟框架都保留独立的单请求层pi-mono/pi-ai completeSimple、OpenCode LLM.stream、Claude Code queryModelWithoutStreaming/queryWithModel、 Codex ModelClientSession.stream;agent loop 只是这一层的循环使用者。
"无工具⇒单轮"不能当永久语义Codex 的继续条件是 model_needs_follow_up || has_pending_input,stop hook / compaction 都能让无工具 turn 继续;Claude Code 同。 所以单次调用要有自己的名字(llm),而不是 agent 的一个退化档位。
分流点必须在 session 设施之前反例 OpenClaw:轻量 promptMode=none 在完整 runner 里较晚生效,session/资源/事件设施照建,省不下固定开销。llm 的路径上不建这些。

6. 边界与取舍

决定理由
两个名字,不是一个 agent() 内部分流调用方的意图本来就是两种;用"profile 带不带工具" 推断执行路径是间接信号,调研证明它在多个框架里不成立。名字直接把意图写进代码。
llm 不带工具参数带上工具它就是另一个 agent 入口,两个原语的边界就没了。 需要工具循环的调用属于 agent。
测试缝放 provider 调用边界公开的 llm()/agent() 不暴露测试参数; 注入发生在 Runtime 构造层(Runtime(call=…) / stream_fn), 两条路共享同一个缝。
runtime.exec 退层不删除它是 llm 的实现材料,也是嵌入 (Runtime(call=mock_llm) 独立运行)与 provider 内核的接口;examples 演示的就是这层,不迁。
runtime 环境获取,不串参数~100 处调用的签名都在串 runtime: Runtime, 这是管道噪音;contextvar 已存在(_current_runtime),llm 直接用。

实现状态

批 1–4 已完成。openprogram.agentic_programming.llm 已导出:环境 Runtime 的单次调用形态,不创建 session 分支或 transcript;str prompt 入口转单个 text block,content blocks 原样进 provider 层;model/effort 仅覆盖本次调用。llm 的 ModelCall 节点与 agent 的 assistant 节点记录 execution_kindprovider_request_countagent_iteration_count,传输重试不计迭代。 agent 与 llm 共用 AgentSession → agent_loop → provider stream 单次调用层。 workflow 注入环境含带 checkpoint 的 llm

迁移结果:本体 agentics 与三个嵌套 harness 仓的单次调用全部改为 llm() (Research 92 处、GUI 23 处、Wiki 1 处、本体约 30 处)。编排层仍在 runtime.exec 上的只剩 9 处带工具循环的调用(deep_work 工作步、llm_call_example 示例、Wiki ingest 生成/enrich、 Research utils/wiki survey/run_experiment 等),均带 tool loop, migrates with agent() in a later batch 注释,等 agent 批次逐处判断。 openprogram/memory/wiki/ 死代码目录(f4147247 换 Markdown workspace 后的磁盘残留, 无调用方)已删除。