当我们谈论 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)