核心摘要

Eino Component 是类型化能力边界,不是底层实现可无条件互换的保证。在 v0.9.21 中,BaseChatModel 提供 Generate 与 Stream;ToolCallingChatModel.WithTools 返回新的 Tool 绑定模型,不修改共享实例;Tool 参数由 ToolInfo.Desc 与 ParamsOneOf 描述;Retriever 统一调用形态,但排序、过滤、授权和一致性仍由实现与应用负责。

核心要点

  • 只有在应用先定义语义契约后,Component Interface 才能成为可靠抽象。
  • 并发服务应使用不可变的 WithTools,替代已弃用且会修改实例的 BindTools。
  • ToolsNode 执行模型提议的调用,但不负责判断调用者是否有权执行。
  • Retrieval Relevance、Source Trust、Freshness 与 Access Control 必须分字段治理。
  • 每个 Provider Adapter 都要执行 Error、Stream、Option、Cancellation 与 Upgrade 契约测试。

组件全景

官方 Components 文档按应用职责组织 Eino 能力:

Component 输入与输出 应用必须补齐的契约
BaseChatModel Messages 到完整响应或响应流 Provider 身份、超时、输出校验
AgenticModel Agentic Messages 到结构化 ContentBlock 特性支持、审批语义、Beta 迁移
Tool JSON 或类型化参数到结果 认证、授权、副作用
Loader Source 到 Documents SSRF 控制、解析上限、来源
Transformer Documents 到转换后 Documents Offset 血缘、删除传播
Embedding Text 到 Vector 模型 Revision、维度、归一化
Indexer Documents 到索引记录 一致性、Tenant、幂等
Retriever Query 到候选 Documents Filter、Score、进入 Context 前的 ACL
ChatTemplate Variables 到 Messages Escape、Role Boundary、Prompt 版本
Lambda 类型化 Go 函数 取消、边界与确定性错误

具体实现通常来自 EinoExt。每个导入模块都要独立固定版本,Core Tag 不会自动锁定全部 Provider Adapter。

v0.9.21 的 Model 接口

固定版本的 components/model/interface.go定义泛型 BaseModel 和两条消息路径:

接口 Message 类型 Tool 绑定方式
BaseChatModel *schema.Message 本身不绑定
ToolCallingChatModel *schema.Message 不可变 WithTools
AgenticModel *schema.AgenticMessage Request-level Option

旧的 ChatModel.BindTools 为兼容保留,但已经弃用。它会修改模型实例,两个绑定不同 Tool 集合的并发请求可能发生竞态;WithTools 返回派生 ToolCallingChatModel,更适合共享服务。

类型系统无法统一 Provider 行为。某个 Adapter 可能支持并行 Tool Call,另一个可能忽略公共 Option,还有的只有在 Stream 结束后才返回 Usage。契约测试应覆盖:

  • 不支持的 Option 与能力协商;
  • Tool Call ID、参数分片和 Finish Reason;
  • 首个 Chunk 前后出现的 Stream Error;
  • Cancellation 与 Provider Timeout;
  • Usage 核算与重试重复;
  • Structured Output 与 Multimodal 行为。

Tool 契约:描述不是权限

ToolsNode 与 Tool 官方指南明确了执行边界:模型把 ToolCalls 写入 Assistant Message,ToolsNode 调用已配置 Tool。模型只选择一个提案,可信代码决定它能否执行。

v0.9.21 的 ToolInfo 使用:

  • Name:唯一可调用标识;
  • Desc:说明何时以及为何调用;
  • ParamsOneOf:轻量参数描述或 JSON Schema 2020-12。

安全执行顺序如下:

sequenceDiagram participant M as Model participant N as ToolsNode participant P as Policy participant S as Source Service M->>N: ToolCall Name 与 Arguments N->>P: 解析后的候选加可信 Principal P-->>N: 允许或拒绝准确动作 N->>S: 有界且幂等的请求 S-->>N: 类型化结果或失败 N-->>M: 限制大小并带来源的结果

Tool Alias 与 Schema 可以改善兼容性,但都不会授予权限。Name、Description、Alias、Argument 和返回内容都应按不可信输入处理。

可运行的类型化 Tool

下面程序以 Eino Core v0.9.21 为目标。InferTool 从 Go Struct 推导参数 Schema,而 Tenant 身份来自可信 Request Context,不来自模型 JSON。

go
package main

import (
	"context"
	"fmt"

	"github.com/cloudwego/eino/components/tool/utils"
)

type principalKey struct{}

type Principal struct {
	TenantID string
}

type LookupOrderInput struct {
	OrderID string `json:"order_id" jsonschema:"required" jsonschema_description:"Order identifier in the caller tenant"`
}

type LookupOrderOutput struct {
	OrderID string `json:"order_id"`
	Status  string `json:"status"`
}

func main() {
	orders := map[string]struct {
		TenantID string
		Status   string
	}{
		"A-42": {TenantID: "tenant-a", Status: "paid"},
	}

	lookup, err := utils.InferTool(
		"lookup_order",
		"Read one order already authorized for the authenticated tenant.",
		func(ctx context.Context, input LookupOrderInput) (LookupOrderOutput, error) {
			principal, ok := ctx.Value(principalKey{}).(Principal)
			if !ok {
				return LookupOrderOutput{}, fmt.Errorf("missing principal")
			}
			order, ok := orders[input.OrderID]
			if !ok || order.TenantID != principal.TenantID {
				return LookupOrderOutput{}, fmt.Errorf("order not found")
			}
			return LookupOrderOutput{OrderID: input.OrderID, Status: order.Status}, nil
		},
	)
	if err != nil {
		panic(err)
	}

	info, err := lookup.Info(context.Background())
	if err != nil {
		panic(err)
	}
	ctx := context.WithValue(context.Background(), principalKey{}, Principal{TenantID: "tenant-a"})
	result, err := lookup.InvokableRun(ctx, `{"order_id":"A-42"}`)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s => %s\n", info.Name, result)
}

预期输出:

text
lookup_order => {"order_id":"A-42","status":"paid"}

Request Context 应使用未导出的类型化 Key。tenant_id、Role、Approval State 或 Authorization Boolean 绝不能以模型参数为事实源。

Retriever 只返回候选

Retriever 返回候选文档,不证明文档真实、最新、完整或对调用者可见。Generator 使用证据前,应归一化以下属性:

字段 价值
document_id 与 revision 稳定引用与失效
source_uri 与 Span Offset Claim-level Provenance
tenant_id 与 Object Scope 排序前授权
score_type 与 Direction 可比较的排序语义
valid_from / valid_to Freshness 与时态问题
Parser / Chunker / Embedding Revision 可复现检索

替换 Elasticsearch、VikingDB 或其他 Adapter 时,应对同一查询集验证 Filter 语义、Tie Order、Duplicate、Score Calibration、Pagination、Timeout、Partial Failure 与 Delete。

组件契约测试

每个实现应包含三层测试:

  1. 接口层:输入类型、Option、Nil、Cancellation 与错误类别。
  2. 语义层:Provider 特性、Score 含义、Tool Schema 与 Metadata。
  3. 系统层:授权、预算、重试、幂等、脱敏与可观察结果。

编译成功只证明类型兼容;Provider 调用成功只证明链路可达。两者都不能证明任务质量或策略合规。

常见失败模式

失败 后果 控制
用 BindTools 修改共享模型 跨请求 Tool 泄漏或竞态 使用 WithTools,增加并发测试
信任模型生成的 Tool 参数 跨 Tenant 读取或危险写入 服务端推导 Principal,校验对象与动作
把 Schema 当业务校验 形状合法但操作错误 解析后执行不变量
仅凭接口替换 Retriever 排序或 Filter 回归 Adapter 契约测试
Callback 记录完整载荷 Secret 或个人数据泄露 脱敏、采样、留存
对所有错误重试 重复副作用和成本增长 类型化 Retry Policy 与幂等

常见问题

Eino 会自动执行模型请求的每个 Tool 吗?

ToolsNode 会针对输入中的调用执行已配置 Tool。应用仍需限制可见 Tool Set,并在执行前后强制授权与副作用策略。

AgenticModel 可以直接替换 BaseChatModel 吗?

不可以。它使用 AgenticMessage ContentBlock,并采用不同 Tool 绑定方式。迁移属于契约变更,应测试每个 Provider 和下游消费者。

Tool 应返回纯文本还是结构化数据?

返回下一步真正需要的最小机器可读结果,同时限制大小、保留来源并排除 Secret。Eino 也提供支持多模态结果的 Enhanced Tool Interface。

Retriever 能否负责访问控制?

某个实现可能支持 Filter,但应用必须从可信身份生成 Filter 并验证其语义。未授权候选不应影响排序、Cache、Log 或模型 Context。

什么逻辑适合放进 Lambda?

输入、输出、取消和错误都明确的轻量有界转换。隐藏网络请求或 Policy Decision 会让 Lambda 难以测试和运维。

一手资料

相关阅读