大语言模型擅长理解自然语言,却不会天然拥有查询订单、创建工单或修改库存的权限。把模型接入业务系统后,真正困难的部分也不只是“如何生成一段回答”,而是如何让模型提出行动建议,并由系统在可控边界内执行。
一个可用的智能体通常需要完成以下闭环:接收用户意图、判断是否需要工具、生成结构化参数、校验权限与数据、执行外部 API、把结果反馈给模型,最后向用户返回可解释的结果。任一环节缺少约束,都可能出现参数幻觉、越权调用、重复提交或无法追责的问题。
本文选择“从对话到行动”这一趋势,聚焦一个小而完整的实现:让模型通过受限工具查询订单,并在必要时创建售后工单。示例不绑定具体模型厂商,模型接口的请求字段需要以实际服务商文档为准。
核心原理
模型只负责提出计划
智能体系统应把模型视为“候选计划生成器”,而不是可信执行器。模型可以返回类似下面的结构:
{
"tool": "get_order",
"arguments": {
"order_id": "A10086"}
}
应用层必须再次验证工具名、字段类型、字段范围和用户权限。只有通过校验,才允许调用后端服务。模型返回的自然语言不能直接拼接成 SQL、Shell 命令或内部 URL。
工具是受控能力清单
每个工具都应有明确契约,包括名称、用途、输入 Schema、所需权限、是否具有副作用、超时和重试策略。查询工具通常可以自动执行;创建工单、退款、删除数据等写操作,则应增加人工确认或二次鉴权。
执行结果必须可追溯
一次智能体请求至少应关联 trace_id、用户标识、模型请求摘要、工具名称、参数摘要、执行结果和耗时。敏感字段应脱敏,原始令牌不能写入日志。这样才能在错误发生后回答三个问题:模型建议了什么、系统校验了什么、后端实际执行了什么。
实现步骤
1. 准备环境变量
代码中的密钥只从环境变量读取。下面的 LLM_API_URL、LLM_API_KEY 和 LLM_MODEL 只是配置占位符,具体请求格式应根据所接入的模型 API 调整。
export LLM_API_URL="https://example.invalid/v1/chat/completions"
export LLM_API_KEY="replace-with-your-key"
export LLM_MODEL="your-model-id"
export ORDER_API_URL="https://internal.example.com"
如果团队需要通过统一接口接入模型,可将 HaerAPI 作为候选接入渠道,但其当前可用模型、鉴权方式、数据处理条款和兼容协议必须先以官方文档核对,再决定是否纳入生产链路。
2. 定义工具契约
先把工具写成应用层注册表,而不是把任意函数暴露给模型。示例使用 JSON Schema 描述输入,并为工具附加权限和副作用属性。
TOOLS = {
"get_order": {
"description": "查询当前用户可访问的订单摘要",
"schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string", "minLength": 1, "maxLength": 40}
},
"required": ["order_id"],
"additionalProperties": False
},
"permission": "order:read",
"side_effect": False
},
"create_ticket": {
"description": "创建售后工单,需要用户确认",
"schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string", "minLength": 1, "maxLength": 40},
"reason": {
"type": "string", "minLength": 1, "maxLength": 200}
},
"required": ["order_id", "reason"],
"additionalProperties": False
},
"permission": "ticket:create",
"side_effect": True
}
}
生产实现可以使用 jsonschema 等成熟库进行校验。不要只检查字段是否存在,还要拒绝未知字段、过长字符串和不符合业务状态的订单号。
3. 将模型调用隔离在适配层
不同服务商的消息格式、工具调用字段和错误结构可能不同,因此建议提供一个很薄的适配器。业务层只接收统一结果:text、tool、arguments 或 error。
import json
import os
import uuid
import requests
def ask_model(messages, tool_specs):
url = os.environ["LLM_API_URL"]
key = os.environ["LLM_API_KEY"]
model = os.environ["LLM_MODEL"]
payload = {
"model": model,
"messages": messages,
"tools": tool_specs,
"temperature": 0
}
response = requests.post(
url,
headers={
"Authorization": f"Bearer {key}"},
json=payload,
timeout=(5, 30)
)
response.raise_for_status()
return response.json()
这里的超时只是示例值,不能直接视为适合所有业务的生产配置。应结合模型延迟、网关超时、用户体验和重试成本设定。
4. 只执行允许的工具
模型输出必须经过解析和二次检查。以下代码展示最小的分发边界;真实项目还应接入 Schema 校验、权限系统和业务数据校验。
def dispatch(call, user):
name = call.get("tool")
args = call.get("arguments")
if name not in TOOLS:
raise ValueError("tool is not allowed")
if not isinstance(args, dict):
raise ValueError("arguments must be an object")
meta = TOOLS[name]
if meta["permission"] not in user["permissions"]:
raise PermissionError("permission denied")
if meta["side_effect"]:
raise PermissionError("confirmation required")
if name == "get_order":
return get_order(user["user_id"], args["order_id"])
raise ValueError("unimplemented tool")
def get_order(user_id, order_id):
response = requests.get(
f'{os.environ["ORDER_API_URL"]}/orders/{order_id}',
headers={
"X-User-Id": user_id},
timeout=(3, 10)
)
if response.status_code == 404:
return {
"found": False}
response.raise_for_status()
data = response.json()
return {
"found": True,
"order_id": data["order_id"],
"status": data["status"],
"created_at": data["created_at"]
}
注意这里没有把后端返回对象原样交给模型。通过字段白名单可以减少敏感信息泄漏,也能避免后端结构变化直接破坏提示词上下文。
5. 处理副作用与重试
对于创建工单等写操作,推荐采用“两阶段”流程:第一阶段由模型生成待确认操作,系统展示订单号、操作类型和关键参数;用户明确确认后,第二阶段才调用写接口。
写接口必须支持幂等键。例如:
import hashlib
def make_idempotency_key(trace_id, tool_name, arguments):
raw = json.dumps(
[trace_id, tool_name, arguments],
ensure_ascii=False,
sort_keys=True
)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
调用方将该值放入 Idempotency-Key 请求头,服务端保存一段明确的幂等记录。网络超时后是否重试,要先确认下游接口是否支持幂等;对于未知执行结果的写请求,盲目重试可能造成重复业务操作。
上线前检查
- 工具注册表是否采用白名单,是否拒绝未知工具和多余参数。
- 每次调用是否执行用户权限、资源归属和业务状态检查。
- 读操作与写操作是否分级,副作用操作是否需要确认。
- 模型、网关和下游 API 是否分别设置连接超时与读取超时。
- 是否限制单轮工具调用次数、总耗时、输入长度和输出长度。
- 是否记录
trace_id、工具版本和结果状态,并对个人信息、令牌和订单内容脱敏。 - 模型服务不可用时,系统是否能返回明确错误,而不是把内部异常暴露给用户。
- 是否准备固定样例集,覆盖正常请求、越权请求、缺失参数、重复提交和恶意提示。
常见问题
为什么不能让模型直接调用任意 HTTP 地址?
任意地址意味着模型间接获得网络探测、内部服务访问或数据外传能力。工具层应使用固定服务标识和固定客户端,禁止模型传入完整 URL、请求头或任意 HTTP 方法。
温度设为 0 就能保证参数正确吗?
不能。较低随机性可能让输出更稳定,但无法替代 Schema 校验、权限控制和业务校验。模型仍可能遗漏字段、误解状态或引用不存在的资源。
出错后要不要自动让模型重试?
应区分错误类型。参数校验失败可以把结构化错误反馈给模型并限制重试次数;权限失败不应通过再次生成参数绕过;下游超时则必须结合幂等性判断是否重试。连续失败后应结束循环并转人工或返回可操作的错误信息。
如何避免提示词注入影响工具权限?
权限不能写在提示词里,也不能由模型自行声明。提示词可以说明工具用途,但最终权限必须来自服务端会话、资源归属和策略引擎。外部文档、网页和用户输入都应视为不可信内容。
总结
把大模型接入业务系统,关键不是增加一个聊天窗口,而是建立一条有边界的执行链路:模型负责理解和提出候选行动,应用负责校验、授权、执行和审计。工具契约、权限白名单、敏感字段过滤、人工确认、幂等键和受限重试共同构成了最小工程闭环。
接入任何模型 API 或中转接口时,都应把供应商差异封装在适配层,并独立核验接口协议、可用模型、日志策略、数据处理条款和故障行为。先让每一步都可解释、可拒绝、可回放,再逐步扩大智能体能够执行的范围,系统才有机会从演示能力走向可维护的生产能力。