Hotdry.
ai-systems

LLM 作为软件函数组件:类型签名、API契约与函数组合模式

面向 Software 3.1 架构范式,探讨将 LLM 输出视为软件函数组件的工程化方法,给出类型签名、API 契约设计与函数组合模式。

当我们谈论 Software 3.1 范式时,核心转变在于将大语言模型从「对话界面」提升为「可组合的软件函数」。传统软件开发中,每个模块都有明确的输入输出类型和契约约束;如今,这种工程化思维同样适用于 LLM 驱动的智能系统。本文将从类型签名、API 契约和函数组合三个层面,阐述如何将 LLM 视为软件工程中的可组合组件。

一、核心类型签名:从概率性函数到 Effectful 视图

在类型论视角下,一次 LLM 调用并非传统的纯函数,而是一个概率性函数。设请求为 R、配置为 C、执行前状态为 S,则一次调用的类型签名可以形式化为:

LLM : R × C × S → D(O × S')

其中 R 包含提示词、消息历史和工具模式定义;C 包含模型标识、温度参数、最大 token 数等配置;S 表示调用前的系统状态(配额、会话上下文、工具注册表);O 是输出 payload(文本 token、工具调用或结构化 JSON);S' 是调用后更新的系统状态;而 D 表示输出上的概率分布 —— 这正是 LLM 与确定性函数的核心区别。

如果将上述签名封装为更符合工程实践的形式,可以采用 Effectful 计算风格:

LLM : Request → Config → State → IO(Response × State)

这种视角将 LLM 视为产生副作用的 IO 操作,Response 是从底层分布中采样得到的具体实例。在实际 SDK 设计中,这一层抽象通常被简化为异步函数调用:

type LLM = (request: LLMRequest) => Promise<LLMResponse>;

其中 LLMRequest 和 LLMResponse 包含明确的类型字段,后续章节将详细展开。

二、API 契约:TypeScript 类型定义与 Design-by-Contract

请求与响应类型

一个实用的 API 契约需要在 JSON Schema 层面固定 Request 和 Response 的结构:

type ToolSchema = {
  name: string;
  description?: string;
  parameters: JSONSchema;
};

type LLMRequest = {
  messages: Message[];
  tools?: ToolSchema[];
  config?: {
    model: string;
    temperature?: number;
    max_tokens?: number;
  };
  state?: StateHandle;
};

type LLMToolCall = {
  name: string;
  arguments: unknown;
};

type LLMResponse = {
  messages: Message[];
  tool_calls?: LLMToolCall[];
  usage?: {
    prompt_tokens: number;
    completion_tokens: number;
    total_tokens: number;
  };
  newState?: StateHandle;
};

上述类型定义构成了 LLM 函数签名的具体形式:LLM : LLMRequest → Promise<LLMResponse>

前后置条件

将 Design-by-Contract 思想引入 LLM 调用,可以为每个函数调用附加形式化的前置条件和后置条件。前置条件约束输入的合法性:

  • messages 必须符合允许的角色(system、user、assistant、tool)和格式规范;
  • 总体 token 长度必须落在模型上下文窗口范围内;
  • tools[*].parameters 必须是合法的 JSON Schema;
  • config.temperature 取值范围为 [0, 2],其他参数亦需满足类型约束。

后置条件则保证输出的可预期性:

  • 每个 tool_call.arguments 必须通过对应工具的 JSON Schema 验证;
  • 若 messages 中声明了「format: JSON」约束,assistant 最终消息必须能解析为有效 JSON 并符合指定 schema;
  • usage 字段间需满足内部一致性(total = prompt + completion)。

可以将一个完整的 LLM API 契约形式化为五元组 C = ⟨I, Pre, Post, S, π⟩,其中 I 是接口(Request, Response 对),Pre 是前置条件集合,Post 是后置条件集合,S 是状态空间,π 是由模型决定的隐式输出分布。

三、函数组合模式:LLM Router 与工具函数的顺序组合

在 Agent 架构中,LLM 的角色往往不是直接生成最终答案,而是作为「路由器」或「规划器」,决定在何时调用何种工具。这一模式可以形式化为以下组合:

工具函数本身是确定性函数:

type Tool = (args: A) => Promise<R>;

LLM 则输出工具调用指令:

type Step =
  | { kind: "assistant"; message: Message }
  | { kind: "tool_call"; name: string; args: unknown }
  | { kind: "final"; result: unknown };

type Agent = (history: Message[], state: S) => Promise<{ step: Step; state: S }>;

组合流程如下:LLM 决策生成 tool_call(name, args);系统校验 args 是否符合工具的 JSON Schema;若校验通过,调用对应工具函数 Tool(name): A → R;将工具返回结果追加到消息历史,再次调用 LLM 生成下一步。

从类型签名角度看,单个 Agent Step 可以表示为:

AgentStep : (H, S) → IO((H', S'))

其中 H 是消息历史,Agent 内部通过组合 LLM 和工具函数完成这一转换。

四、顺序组合规则与契约兼容性

当我们希望从形式化角度推理多个 LLM / 工具步骤的组合时,可以将每个步骤视为带契约的 Effectful 函数。设:

  • C₁ 为「LLM → 工具调用 JSON」的契约;
  • C₂ 为「tool (args) → 结果」的契约;
  • C₃ 为「LLM (history + tool result) → 最终 JSON」的契约。

假设 Post₁ 保证 args 符合工具输入 schema,Pre₂ 恰好要求该 schema;Post₂ 保证 result 符合某个输出 schema,而 Pre₃ 恰要求该 schema 出现在历史记录中。那么组合后的管道满足一个导出的契约:

C = C₁ ; C₂ ; C₃

其中 Pre_C = Pre₁(在该前置条件下所有后续前置条件均被满足),Post_C = Post₃(最终保证,例如生成了有效的领域对象)。

这种基于契约兼容性的组合推理方式,使得 LLM + 工具的复杂工作流可以在不依赖临时提示词的情况下被形式化验证。

五、实践参数与监控要点

将理论框架落地到工程实践时,以下参数值得关注:

模型选择与配置方面,建议在生产环境固定 model 版本而非使用「latest」别名;temperature 默认值设为 0.7,针对需要确定性的任务可降至 0.1 以下;max_tokens 需根据输出 schema 预估并留有冗余。契约验证方面,每次 LLM 返回 tool_calls 前必须执行 JSON Schema 校验,校验失败应触发重试或降级而非直接传递给下游;结构化输出场景推荐使用强制 JSON 模式而非依赖提示词。监控指标应覆盖 token 消耗量(用于成本控制)、工具调用成功率、契约校验失败率以及端到端延迟分布。

通过将 LLM 视为带有类型签名和 API 契约的软件函数组件,开发者可以在保持灵活性的同时获得工程化的可预期性与可组合性。这一思路正是 Software 3.1 架构范式的核心价值所在。


参考资料

  • Contracts for Large Language Model APIs (Tanzim Hossain Romel)
  • LAPIS: Lightweight API Specification for Intelligent Systems (arXiv)
查看归档