技能里调用平台大模型
什么时候读:技能内部要调用「本角色绑定的那个大模型」时(二次推理、内容改写、分类打分等)。 来源:executor.py
execute/_llm_env_for/_build_subprocess_env(2026-08 核实)。 provider 真实接口已核(base.pyLLMProvider);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 Python | ctx["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) | await | CompletionResult:.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_KEY | provider 非 anthropic(openai/deepseek/doubao/moonshot/custom/xkeyai 都按 OpenAI 协议) | key |
OPENAI_BASE_URL | 同上且配了 base | base 补到 /v1(缺则加,已带不动) |
ANTHROPIC_API_KEY | provider = anthropic | key |
ANTHROPIC_BASE_URL | 同上且配了 base | base(裸 base,不补 /v1) |
MYINC_LLM_KEY | 有 key | key |
MYINC_LLM_BASE | 有 base | openai 支带 /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)
三个坑
-
无 key 的模型(如 ollama)一个变量都不注入。
_llm_env_for在if not key: return {}。所以子进程技能取不到任何 LLM 变量是正常情况(角色配了本地无 key 模型),不是故障——按「无可用模型」优雅降级,别报错崩掉。 -
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,它们各自是对的。 -
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"](内部字段,子进程里也没有)