Gotchas
A don't-step-here list for LLMs and agents — every item maps to real spineagent code behavior.
给 LLM / agent 的「别踩」清单。每条都对应真实代码行为。
1) provider 的输出是 OpenAI ChatCompletion 形状(不是各家 native)
所有 provider 适配器都实现 corespine 的 LLMProvider 协议:chat(messages, *, tools=None) 返回
corespine.llm.provider.ChatCompletion,统一 OpenAI 形状——无论后端是 Anthropic / Gemini /
Cohere / Bedrock 还是 OpenAI 兼容端点。取结果固定这么写:
completion.choices[0].message.content # 文本(可能为 None)
completion.choices[0].message.tool_calls # tuple[ToolCall, ...] | None
completion.choices[0].message.tool_calls[0].function.name # 工具名
completion.choices[0].message.tool_calls[0].function.arguments # JSON 字符串(不是 dict!)
completion.choices[0].finish_reason # 'stop' / 'tool_calls' / 'length' / 'content_filter'
completion.usage.prompt_tokens / .completion_tokens / .total_tokens # usage 可能为 Nonefunction.arguments是 JSON 字符串,要json.loads(...)才是 dict(FunctionCallingAgent内部已这么做)。- 别假设拿到的是某家 native 响应对象——native → OpenAI 的转换在各适配器内部完成,对外只有这一种形状。
2) 离线默认是 MockProvider,它不会 function-calling
不传真实 provider 时,默认走 corespine.MockProvider:对同一对话恒定回
[mock:<hex>] <最后一条 user 文本>。它故意不伪造 tool_calls(离线不假装会推理)。后果:
LlmAgent(name, MockProvider())只做确定性回声,适合跑通管道 / 断言,不会真做规划或工具选择。FunctionCallingAgent(..., MockProvider(), tools=[...])因 mock 不回 tool_calls,会第一步就出文本, 绝不进工具循环。要演示 / 测试 function-calling 循环,注入一个会回tool_calls的 provider(真实 后端,或脚本化 fake——见 recipes §10)。- 离线想要确定性的「工具循环」,用
ToolUsingAgent+SyntaxToolPolicy(按<tool>: <arg>语法路由), 而不是FunctionCallingAgent。
3) 真实后端要装对应 extra(否则友好报错,而非裸 ModuleNotFoundError)
import spineagent 绝不拉任何网络 SDK。真实后端在构造适配器(且未注入 client=)时才延迟 import:
| 后端 | provider | 安装 |
|---|---|---|
| OpenAI / 一切 OpenAI 兼容端点 | OpenAICompatProvider | pip install "spineagent[openai]" |
| Anthropic | AnthropicProvider | pip install "spineagent[anthropic]" |
| Cohere | CohereProvider | pip install "spineagent[cohere]" |
| Google Gemini | GeminiProvider | pip install "spineagent[gemini]" |
| AWS Bedrock | BedrockConverseProvider | pip install "spineagent[bedrock]" |
| MCP 真实 SDK | mcp_clients.make("real") / load_mcp_sdk() | pip install "spineagent[mcp]" |
| A2A 真实 SDK | a2a_agents.make("real") / load_a2a_sdk() | pip install "spineagent[a2a]" |
| 全部 | — | pip install "spineagent[all]" |
缺对应 extra 去构造真实适配器,会得到指明「pip install spineagent[<extra>]」的友好 ImportError。
注意:[gemini] 装的是 google-genai,[a2a] 装的是 a2a-sdk(import 名 a2a)。
4) SeamError 表示「缝槽存在但真实实现未接入」
家族统一约定:某些缝的 real / llm 槽是占位,本包只提供缝 + 离线 stub:
tool_policies.make("llm")—— 直接抛corespine.errors.SeamError(无 SDK 可 import,留待接真 provider 解析 function-calling 后接入)。mcp_clients.make("real")/a2a_agents.make("real")—— 两阶段:未装对应 extra 时先抛ImportError(缺[mcp]/[a2a]);装了 extra 后才走到SeamError(SDK 在但适配器留待使用者 按官方 SDK 接入)。
经验法则:ImportError = 缺 extra(照提示 pip install spineagent[<extra>]);SeamError = 你选了
一个尚未接入的真实槽。两者都意味着:要么改用离线默认(offline),要么自己接入真实实现。
5) FunctionTool 与 Tool 是两种东西,别混用
Tool协议:run(arg: str) -> ToolResult。给ToolUsingAgent(离线语法路由)用。例:CalcTool、EchoTool、McpClientTool、AgentTool。FunctionTool(及@function_tool):schema()+invoke(args: dict) -> str。给FunctionCallingAgent(真 LLM function-calling)用。它不实现Tool.run。
把 @function_tool 装出来的对象丢进 ToolUsingAgent(或反过来把 CalcTool 丢进 FunctionCallingAgent)
会出错——它们走的是不同的工具协议。
6) $prev 只在 ToolUsingAgent 里、且首步替换为空串
ToolUsingAgent 在执行工具前把参数里的字面量 $prev 替换为上一步观测的输出。若首步就引用
$prev(尚无上一步),替换为空串——余下参数能否被工具处理由工具自身决定(如 CalcTool 对空串会抛
ValueError,异常照常上抛)。错误处理 / 重试不在本层(交编排层 / 调用方)。$prev 是 ToolUsingAgent
的语义,不适用于 FunctionCallingAgent(后者由模型自己串联多轮)。
7) max_steps 计的是工具调用次数,不是 LLM 轮数语义要分清
ToolUsingAgent(max_steps=N):N = 最多调用多少次工具;收尾决策本身不占预算。触顶强制收尾、绝不死循环。FunctionCallingAgent(max_steps=N):N = 最多 chat 轮数(每轮一次model.chat);触顶兜底返回"(reached max_steps without a final answer)"。两者都保证产出非空。
8) 编排默认 fail-fast;弹性容错要显式开 resilient
Coordinator.run_*(...) 默认 resilient=False——任一 agent 抛异常即冒泡。要「坏 agent 不炸整批」,
传 resilient=True:异常被归一为 error_to_dict(exc) 塞进该步 AgentResult.error(r.ok 为 False),
顺序 / 并行跑完其余 agent;流水线则在失败处停止(下游拿不到输入)。ChainAgent.step 内部走流水线
但不透传 resilient,始终 fail-fast。
9) trace 只吃元数据,塞正文会被拒
任何 step / 编排方法可选的 trace= 只接受元数据(agent 名、步序、计数、长度、耗时)。
corespine.InProcessPrivacyTraceSink 「构造即保证」:写入命中 FORBIDDEN_KEYS(content / text /
answer / value / prompt / completion …)的字段会当场抛 TraceError。所以别期望从 trace 里
读回任务或输出正文——那是设计上挡死的。
10) provider 默认 max_tokens 与默认 model 是真实的、要留意
各 provider 有真实默认值(来自源码),用前确认是否符合预期:
AnthropicProvider默认model="claude-opus-4-8"、max_tokens=4096。OpenAICompatProvider/BedrockConverseProvider:model必填(无通用默认);max_tokens默认 4096(Bedrock 经extra传)。CohereProvider默认model="command-r-plus";GeminiProvider默认model="gemini-2.5-flash"。- 需要 thinking / 流式 / 其它后端特有参数,经各 provider 的
extra=透传给底层 SDK 调用。reasoning / citations 等扩展本期不透出(统一规整为 OpenAI 形状时丢弃)。
11) 不要指望从 spineagent 拿 RAG
spineagent 不含任何 RAG 概念,也不在包层面依赖 ragspine。要做检索增强,在运行时把
ragspine(或任意检索能力)包成一个实现了 Tool(run(arg)->ToolResult)或 MCP server 的适配器,
插给某个 agent——方向只能 spineagent → ragspine,绝不写进 dependencies。
12) InProcessSandbox 是白名单表达式求值器,不是通用 Python 沙箱
InProcessSandbox 把 code 当纯表达式白名单迷你语言跑:只认极小内建白名单
(len/str/int/float/bool/abs/round/min/max/sum/sorted/repr/list/dict/tuple/set),故意没有
open/eval/exec/__import__/getattr/range,拒绝 import / 属性访问 / 任意调用 / 非白名单名字。
- 别拿它跑语句 / 副作用代码 / 循环 / import;它只求值单个受限表达式,自由名只能来自
env=。 - 失败(语法 / 被拒 / 触限 / 运行时错)一律不抛异常,归一为非零
SandboxResult(error原因码syntax/disallowed/limit_exceeded/error);要判成败看result.ok,别去try/except。 subprocess/container是桩:sandboxes.make("subprocess")/make("container")直接抛SeamError(container先经load_container_sdk()延迟 importdocker)。真实隔离后端留待使用者自接。
13) Skill 脚本受 Sandbox 白名单约束;桥出来的是 FunctionTool 不是 Tool
FixtureSkill.invoke(args) 把脚本经 Sandbox.run(script, env=args) 跑——默认 InProcessSandbox,所以
脚本同样只能是上面那套白名单表达式(args 当自由名)。skill_as_function_tool(skill) 桥出来的是
FunctionTool(schema() + invoke(dict)),给 FunctionCallingAgent 用,不实现 Tool.run,
不能丢进 ToolUsingAgent(同 §5 的 FunctionTool vs Tool 之分)。
14) Middleware 只有 before_step / after_step,没有 wrap
Middleware 协议就两个钩子:before_step(ctx) 和 after_step(ctx, result) -> AgentResult——没有 wrap。
MiddlewareAgent 正序进 before_step、逆序出 after_step(洋葱环),最后把 result.agent 重盖为
MiddlewareAgent 自己的 name(provenance 归外层,别指望还是内层 agent 名)。四件套只往 trace 记元数据。
15) AgentResult.artifacts 是引用不是字节;BlobArtifactSink 要注入 store
AgentResult.artifacts是tuple[ArtifactRef, ...]——轻量引用(key/sink/name/mime/producer/size), 不含字节。要拿真正的字节 / 文本,用对应ArtifactSink.fetch(ref) -> Artifact(再取.data/.text)。BlobArtifactSink(store=...)必须注入一个 corespineBlobStore;artifact_sinks.make("blob")不给store=会失败。元数据全在ArtifactRef上,BlobStore只承诺字节往返(put/get),别指望它存元数据。- 默认路径不产出 artifacts(空 tuple);只有用了 artifact sink 的 agent 才填。
16) 流式是可选加成协议,只有两家实现——用 isinstance 检测
stream_chat(...) 来自 corespine 的 StreamingLLMProvider,不是 LLMProvider 的必备方法。只有
OpenAICompatProvider / AnthropicProvider 实现它(Anthropic 把 native 事件转成 OpenAI 形状块,仅文本、
tool_use 流式未实现);Cohere / Gemini / Bedrock 诚实不实现。所以别无脑 provider.stream_chat(...),
先 isinstance(provider, StreamingLLMProvider) 检测;拼接各块 delta.content 恒等于非流式 chat() 的 content。
17) Agent.name / Tool.name 是只读 property,不是可写变量
Agent / Tool 协议里 name 现声明为只读 @property(@property def name(self) -> str),
为的是满足 mypy --strict 的型变(只读属性对「@property 实现」和「纯类属性 name = "echo"」都兼容;
声明成可写变量则会型变冲突)。语义上不改任何具体实现:具体工具仍用纯类属性(EchoTool.name = "echo")。
经验:按协议消费 agent / tool 时,把 name 当只读读,别去给协议类型的对象赋 .name。