跳到主要内容

技能里调用平台大模型

什么时候读:技能内部要调用「本角色绑定的那个大模型」时(二次推理、内容改写、分类打分等)。 来源:executor.py execute / _llm_env_for / _build_subprocess_env(2026-08 核实)。 provider 真实接口已核(base.py LLMProvider);async 后台派发路径已于 2026-10 按 roles_chat_helpers._run_async_skill 核对,见文末。

注入机制(平台侧,技能不用自己做)​

executor.execute() 跑技能前用 _role_provider(role_id) 解析角色模型、 build_provider_from_config 构建 provider 对象,一次解析注入两份: ctx["provider"](对象,进程内用)和 ctx["_llm_conn"](带 key 的中间 dict,_ 前缀不进子进程, 仅供 executor 内部还原成环境变量)。无 key 模型(ollama)_llm_conn 为空 → 子进程无变量, 但 provider 对象仍注入。注入按 role_id 走,call_skill 的子技能拿自己 role_id 的 provider。

一句话​

平台把「当前角色配的模型」按两种形态注入,按技能形态二选一:

技能形态拿什么怎么拿
进程内 async Pythonctx["provider"](对象)直接读 runtime_context
子进程 / CLI / 其它语言环境变量MYINC_LLM_* / OPENAI_* / ANTHROPIC_*

关键:进程内的 provider 对象进不了子进程。 _build_subprocess_env 过滤掉所有非字符串的 ctx 值,子进程只有环境变量。反过来,进程内技能也能读这些环境变量,但有 provider 对象更直接。

进程内:ctx["provider"]​

from backend.infra.providers.base import Message # 技能可 import backend(datatable/describe_self 已在用)

async def execute(input_data, ctx):
provider = ctx.get("provider")
if provider is None:
return {"success": False, "error_type": "config", "non_retriable": True,
"error": "当前角色未配置可用模型(provider 为空)",
"user_message": "当前角色还没绑定大模型,请在角色设置里配置后重试。"}

resp = await provider.complete(
[Message(role="user", content=prompt)],
system="你是……", # 可选
max_tokens=1024, # 可选
temperature=0.3, # 可选
)
text = resp.text # CompletionResult.text = 完整回复文本
return {"success": True, "result": text,
"tokens": {"in": resp.input_tokens, "out": resp.output_tokens}}

provider 由 _role_provider(role_id) 解析,取角色绑定的 ModelConfig(build_provider_from_config)。取不到为 None——必须判空,不判空直接调会 AttributeError,被兜底成「技能内部错误」。

不预取 provider 的技能(executor _NO_LLM_SKILLS,按技能名硬编码):datatable、trade-bot、trade-hltrade、trade-okxbn-kline、 trade-macro、trade-regimecalc、trade-factor、trade-backtest。这些技能里 ctx["provider"] 恒为 None、也没有 LLM 环境变量。 新技能要调模型就别取这些名字;已在名单里的技能若要加模型调用,得同步改 executor 的名单。

真实接口(backend/infra/providers/base.py LLMProvider 抽象基类,2026-08 核实):

方法 / 属性用法返回
complete(messages, *, system, max_tokens, temperature, options)awaitCompletionResult:.text / .stop_reason / .input_tokens / .output_tokens
stream(messages, *, system, tools, max_tokens, temperature, options, response_format)async for tok in ...逐 str token
tool_call(messages, tools=..., ...)await文本 + tool_call 列表
model / provider_name属性模型名 / anthropic|openai|ollama
last_usage.input_tokens / .output_tokens属性上次调用用量
  • 技能内二次推理首选 complete()——一次拿完整文本,不用拼 stream。面向用户流式才用 stream()。
  • messages 是 list[Message];Message(role, content) 是 dataclass(role ∈ user/assistant/system/tool,content 可为 str 或多模态 blocks)。system 走独立参数,不放进 messages。
  • 临时换模型/调参用 CallOptions:from backend.infra.providers.base import CallOptions, await provider.complete(msgs, options=CallOptions(model="deepseek-reasoner", temperature=0.9));不传 = 用角色 role.toml 默认。

子进程:环境变量​

_llm_env_for 把角色模型暴露成标准 SDK 变量,主流技能不改一行、用官方 SDK 就能调本角色的模型:

变量何时有值
OPENAI_API_KEYprovider 非 anthropic(openai/deepseek/doubao/moonshot/custom/xkeyai 都按 OpenAI 协议)key
OPENAI_BASE_URL同上且配了 basebase 补到 /v1(缺则加,已带不动)
ANTHROPIC_API_KEYprovider = anthropickey
ANTHROPIC_BASE_URL同上且配了 basebase(裸 base,不补 /v1)
MYINC_LLM_KEY有 keykey
MYINC_LLM_BASE有 baseopenai 支带 /v1,anthropic 支裸 base(见下「坑」)
MYINC_LLM_MODEL配了模型模型名(唯一带模型名的变量,标准 SDK 变量只给 base/key 不给 model)
import os
key = os.environ.get("MYINC_LLM_KEY") or os.environ.get("OPENAI_API_KEY", "")
base = os.environ.get("MYINC_LLM_BASE") or os.environ.get("OPENAI_BASE_URL", "")
model = os.environ.get("MYINC_LLM_MODEL", "")
if not key:
print(json.dumps({"success": False, "error_type": "config", "non_retriable": True,
"error": "无可用模型(未注入 LLM 环境变量)"}))
sys.exit(0)

三个坑​

  1. 无 key 的模型(如 ollama)一个变量都不注入。 _llm_env_for 在 if not key: return {}。所以子进程技能取不到任何 LLM 变量是正常情况(角色配了本地无 key 模型),不是故障——按「无可用模型」优雅降级,别报错崩掉。

  2. MYINC_LLM_BASE 的 /v1 不一致(源码实测,不是笔误)。openai 协议支:base 被就地改成 base+"/v1" 后才写入,所以 MYINC_LLM_BASE 带 /v1;anthropic 支:写的是裸 base,不带 /v1。用 MYINC_LLM_BASE 自己拼 URL 时注意这个差异;能用官方 SDK 就用 OPENAI_BASE_URL / ANTHROPIC_BASE_URL,它们各自是对的。

  3. provider 对象不透传、不进子进程、call_skill 不带。 见下。

与 call_skill / 子进程边界的关系​

  • provider 对象是不可序列化的活对象,和 db 一样进不了子进程(_build_subprocess_env 剔除),也不在 call_skill 透传白名单里。
  • 子技能需要模型时,由平台按子技能自己的 role_id 重新解析 provider 注入——不继承调用方的。所以 call_skill 调一个要用模型的子技能,不用(也没法)手动传 provider。
  • _llm_conn(带 key 的中间 dict)是 _ 开头,不进子进程 SKILL_CONTEXT,只用于在 executor 内部还原成环境变量。技能不要去读 ctx["_llm_conn"]。

async 后台技能(已定案)​

后台派发代码在 roles_chat_helpers._run_async_skill:它新建 SkillExecutor() 并调用 executor.execute(skill_name=…, role_id=…, …)—— 与对话线同一个入口。provider / _llm_conn 正是在 executor.execute() 里按 role_id 解析注入的(已按 executor.py 核对),所以后台技能同样拿得到 ctx["provider"],子进程型同样拿得到 MYINC_LLM_* 等环境变量。

照常判空即可(角色没配模型时仍为 None)。后台 ctx 的其它差异(无 user_id / team_id / request,abort_signal=None) 见 runtime-context.md §9。

checklist​

  • 用模型前判 ctx["provider"] / 环境变量是否为空,空则 error_type=config + non_retriable,不崩
  • 进程内用 ctx["provider"];子进程用 MYINC_LLM_* / OPENAI_* / ANTHROPIC_*,不指望子进程有 provider 对象
  • ollama 等无 key 模型 → 无变量是正常,优雅降级
  • 自己拼 URL 时留意 MYINC_LLM_BASE 的 /v1 两支不一致;能用官方 SDK 就用标准变量
  • 不读 ctx["_llm_conn"](内部字段,子进程里也没有)