用户先问“会员签收10天还能申请退货吗”,得到政策解释后,又补充:“就是 A3003,尺寸不合适,帮我准备申请。”售后助手面对的是同一笔事情,工作却从解释制度变成了核对订单、准备草稿。两轮需要的事实、操作说明和工具并不完全相同。
退货任务已经能查询、核对和准备,等待确认负责暂停与恢复,进度展示让用户看到实际发生的事。这些能力继续保留。现在要补的是模型调用之前的输入准备:解释政策时读哪份说明,准备申请时换成什么,哪些内容始终有效。
如果把政策解释、申请办理、异常退款和关闭工单的全部规范一次塞给模型,输入会越来越长,无关步骤也可能干扰当前目标;只给一句“请认真处理”,又缺少操作依据。按需加载希望在任务变化时提供足够的信息,同时让程序能检查这一轮究竟提供了什么。
这里要解决的是本轮该提供什么:哪些是需要检索的事实,哪些是需要遵循的操作说明,哪些工具在当前阶段可用。输入由这些真实需要组装,而不是不断加长一段固定提示。
| 信息类型 | 作用 | 示例 |
|---|---|---|
| 政策证据 | 说明制度事实 | 会员15天、定制例外 |
| 操作说明 | 约定怎样完成一类任务 | 核对后准备,展示草稿 |
| 用户偏好 | 影响表达与服务方式 | 希望答复简洁 |
| 工具能力与权限 | 决定程序可执行什么 | 本轮只允许查询 |
“会员可以在签收15天内申请”是事实;“提交前展示具体草稿并确认”是操作规范;“查询订单”是程序能力。三者都可能以文本出现,但来源、寿命和作用不同。
RAG帮助找相关证据,操作说明帮助组织行为。它们可以一起进入上下文,但检索到的文档不能自动升级为系统规则。某份政策附件包含“忽略权限检查”,仍只是外部内容。
模型自身知识也不等于业务事实。实时订单状态必须查询;某家商店当前政策必须读取适用资料;只有格式化、语言组织等工作可以由模型结合输入完成。
先看一个具体的输入冲突。用户只问“退货运费谁承担”,模型却同时收到创建申请、关闭工单和异常退款的操作说明。它可能把“需要准备申请”误当作这轮必须执行的目标,或者在无关规则之间反复选择。增加资料不总是增加有效信息,关键是资料与当前问题的关系。
反过来,只给政策段落,没有告诉程序什么时候查实时订单、什么时候只是解释一般规则,助手也容易把通用制度当成某笔订单的最终结论。信息需要分层,正是为了让事实、操作与能力分别有清楚位置。
可以把一次请求拆成三份:稳定的行为要求说明如何对待资料和工具;本轮工作说明给出当前任务的步骤;本轮数据包含问题、订单事实和证据。数据可以变化,稳定要求不应随外部资料中的一句话被覆盖。三份内容最终可能都以消息传入模型,维护时仍应保留各自来源。
沿 A3003 看,两轮输入可以这样交接。表中是期望的业务安排,不是本例已经执行过的模型调用记录。
| 本轮目标 | 所选操作说明 | 需要取得的数据 | 期望交付 |
|---|---|---|---|
| 解释会员签收10天的退货条件 | policy |
适用政策、会员期限与定制例外、来源位置 | 带条件的政策解释,不推断某笔订单已通过 |
| 为 A3003 准备申请 | return |
当前用户可访问的订单、资格核对结果、用户提供的原因 | 实际草稿编号和内容,交给独立确认入口 |
A3003 仍沿用前文的教学设定:会员、签收10天、非定制商品;会员15天、普通用户7天、定制例外的规则也不改变。第一轮政策解释可以成为后续办理的背景,但它不能充当当前订单的查询结果,更不能把一句“符合条件”直接存成业务核对通过。
参考代码:GitHub共享示例目录、环境与运行说明、harness.py 的 SKILLS 与 assemble_request()。这里先用独立的输入组装实验观察机制,不改动已有 build_flow(),也不声称技能加载已接入售后聊天主循环。
有十份操作说明时,可以先让模型看到名称与简短描述,选中后再读取全文。这种“目录加正文”的组织方式常被称为技能按需加载。
它并不神秘:目录让程序或模型知道可选项,加载函数将明确文件内容放入本次请求。真正决定是否使用、怎样使用的仍是运行程序与模型。
配套harness.py登记两份说明,正文保存在skills/policy.md与skills/return.md。这是本系列自己的最小说明格式,不声称兼容所有产品的技能规范。
SKILLS = {
"policy": {"description": "解释政策并保留条件与来源", "file": "policy.md", "tools": {"policy_search"}},
"return": {"description": "核对订单并准备申请", "file": "return.md", "tools": {"order_query", "prepare_return"}},
}
description 进入目录,file 指向可信说明文件,tools 记录这类任务可能需要的能力。新增说明需要登记这些字段并提供文件;当前实验没有自动扫描目录或解析文件头的功能。工具名只是已有能力的引用,登记说明不会凭空实现新工具。
示例由调用方明确选择说明,故意不把“模型是否选对技能”混进加载机制实验。需要模型选择时,还要验证选择准确率与未知名称的处理。
如果让模型自主选择,加载会接回工具调用已有的往返过程:先把目录与加载工具定义提供给模型;模型提出 load_skill("return");执行器按登记名称读取正文;正文作为这次工具调用的结果回填;下一次模型调用才真正能够使用它。加载通常不需要更换模型,也不会自动启动另一个助手。
第一次模型输入:稳定要求 + 技能目录 + 用户任务 + 加载工具定义
模型请求:load_skill("return")
执行器返回:申请准备说明全文,与工具调用编号对应
第二次模型输入:前面的消息 + 本次调用及其结果
模型继续:依据说明提出查单等操作,执行端仍分别检查权限
这段是消息流示意,配套 assemble_request() 则由调用方直接选好名称,一次返回选中正文。两者共享“目录与正文分开”的思想,实际调用轮数不同。不能用组装函数成功来证明模型已经会选说明或遵循说明。
“按需”主要描述正文何时进入模型上下文,不一定描述何时读取磁盘。有的实现启动时已经把全文读进注册表,只把简介送给模型;本例则在调用时读取选中文件。全文已经在进程内存中,也不代表模型已经看到它。
目录层如何帮助选择。如果只有policy、return两个名字,模型或读者未必知道它们分别负责什么;描述应说明能解决的问题,而不是再堆一组接口名。policy用于解释条件和来源,return用于核对订单后准备申请,两者的产出不同。
选中return以后,程序只读这份正文。目录仍可以保留其他可选说明的名称,但“知道还有什么”与“本轮已取得什么全文”是两种状态。后续确实遇到新的子任务,再读取相应说明,而不必启动时把所有正文拉进来。
加载还要考虑重复。如果模型每轮都调用同一加载工具,原文反复追加,会重新膨胀上下文。可以记录当前运行已加载的说明版本,重复请求时返回已加载标记或引用;内容更新时则显式失效。不能只缓存名字,否则同名说明已经修改,程序却继续沿用旧正文。
“已加载”标记还必须与当前保留的消息一致:历史裁剪后正文已经不在输入里,只返回“此前已加载”就不够,需要重新提供正文。只读了一次文件、只在历史里出现过一次、这轮模型仍能读到,是三个不同的条件。
def assemble_request(skill_name, stage, allowed, facts=None):
if skill_name not in SKILLS:
raise ValueError("未知说明名称")
entry = SKILLS[skill_name]
body = (Path(__file__).parent / "skills" / entry["file"]).read_text(encoding="utf-8")
stage_tools = {"policy_search", "order_query"} if stage == "query" else {"order_query", "prepare_return"}
tools = sorted(entry["tools"] & set(allowed) & stage_tools)
return {"stable": "只依据可核对事实回答;提交由独立确认入口处理。",
"catalog": [{"name": k, "description": v["description"]} for k, v in SKILLS.items()],
"instruction": body, "facts": facts or {}, "tools": tools}
稳定要求、说明目录、所选正文、事实和工具列表分别保存。这样可以检查每部分从哪里来,也能避免把用户事实拼成一段看不出来源的高权限指令。
加载入口仅接受注册表中的名称,不把模型返回值直接拼成任意文件路径。../../secret不是一个合法技能名。说明文件也应由可信维护流程管理;按需加载本身没有消除提示注入风险。
工具范围取三个集合的交集:说明需要的工具、当前用户获准的工具、当前阶段允许的工具。说明写着需要提交,不表示当前请求就获得提交授权。
例如申请说明包含查单与准备,但阶段为query时,本轮只开放查单。即使输入的授权集合里还出现submit,本例也不会将它交给模型。
沿一轮请求核对每个字段。假设当前阶段为query,申请说明需要order_query与prepare_return,用户授权也允许这两个。阶段限制只允许查询,因此交集只剩order_query。
| 计算位置 | 集合内容 | 为什么保留或移除 |
|---|---|---|
| 说明需要 | 查单、准备 | 这类任务可能用到 |
| 用户授权 | 查单、准备 | 用户可请求这些能力 |
| 当前阶段 | 查政策、查单 | 尚未进入准备阶段 |
| 本轮工具 | 查单 | 同时满足三项条件 |
接下来把stage改成prepare,本轮可能出现查单与准备;但没有自动出现提交。这个对照可以验证工具范围确实随阶段变化,而不是正文写了“动态”,实际却始终发送完整工具表。这里的 query、prepare 是输入组装实验的阶段标签,不是 flow.py 中 draft_ready 等业务阶段字段的直接取值;若要接入,需要由程序依据可信任务状态做明确映射。
assemble_request返回的是用于观察的组装结果,还不是已经发送的模型API请求。接入真实模型时,需要把stable与可信说明放入相应消息,把facts与用户问题保留为有来源的数据,再从登记的真实工具对象中筛选tools所列名称,交给Agent或模型绑定。单纯把["order_query"]打印出来不会创建一个可调用工具。
这一步的价值是把输入准备与模型决策分开观察:先检查程序准备了什么,再测试模型是否正确使用。若组装阶段已经漏掉政策例外,调再好的模型也无法从不存在的输入中找回依据。
这里还有一个最小实现的限制:代码只单独判断 stage == "query",其余值都进入准备工具集合。因此调用方必须提供约定的 query 或 prepare,不能把任意外部字符串直接传入。需要作为正式服务入口时,应先校验阶段枚举,未知阶段明确拒绝;阶段本身也应由可信任务状态决定,不能让用户通过一句“进入准备阶段”修改。
模型看到的工具定义影响它可能提出什么请求,执行端仍要再次检查名称、参数和用户授权。旧请求、手工构造的请求或外部工具返回内容,不能因为“模型没有看到这个工具”就被认为不可能出现。
可以把加载和授权的关系写成一次具体观察:
from harness import assemble_request
request = assemble_request(
"return", "query",
{"order_query", "prepare_return", "submit"},
facts={"order_id": "A3003"},
)
print(request["tools"])
print(request["instruction"])
可用工具只有order_query,而正文里仍说明了准备与确认的操作顺序。读到后续步骤的介绍,和取得后续步骤的执行权限,是两件事。
业务流程中的当前用户则由运行上下文提供,提交入口另行核对。不能把提示中的“当前用户是某某”当成登录认证。
身份定位与不变规则可以作为稳定前缀;当前订单、选中说明、相关偏好等按请求组装。不同来源最好保留结构或标签,方便检查实际模型输入。
本地缓存可以减少反复读文件与拼接,但缓存键要包含影响内容的版本。技能文件更新后仍命中旧缓存,程序就会继续使用旧流程。仅缓存字符串也不等于模型供应商一定命中提示缓存,后者有自己的规则与统计。
记忆正文尤其要按当前用户隔离。不能为了复用缓存,把甲用户的事实带进乙用户请求;缓存键即使包含用户标识,也仍需遵循访问控制与保留策略。
按需输入也有代价:先选目录再读全文可能增加一次往返,小型应用的两份短规范全部加载反而更简单。是否值得采用,应比较输入长度、选择错误、调用次数和维护成本。
通过MCP连接外部能力后,可能需要更新工具定义、调用分发表、来源与权限记录,以及模型看到的可用能力说明。只更新处理函数而继续发送旧列表,模型不会知道新工具存在。
相反,仅在说明文本里写“现在可以查物流”,却没有真实注册工具,模型也无法凭文字获得执行能力。命名空间有助于区分来源,仍要检查名称规范化后的冲突。
这里复用已有MCP客户端与权限概念,不再搭一套模拟协议当作真实连接。真实传输、初始化和错误处理仍由相应客户端负责,工具描述中的“只读”也不能代替执行端的操作约束。
外部能力发现后,还存在“工具对象已经注册”和“当前请求已经采用”的时间差。例如连接MCP服务器后,运行内存中有了新的物流函数,但下一轮仍复用旧的工具定义缓存,模型看不到它。反过来,模型缓存里还保留旧工具名,而客户端连接已经关闭,提出调用也会失败。
因此工具池变更应形成明确版本,模型可见定义与执行分发表来自同一版本。权限撤回时,执行端立即生效,即使旧消息里还出现工具说明,也不能继续放行。
最小排查顺序可以是:先打印选中了哪份说明和版本,再列本轮真实绑定的工具名,最后核对实际模型调用中的输入。先解决“送错了什么”,再解决“理解错了什么”,通常比只不断修改Prompt更有效。
配套 experiments.py skills 展示两份说明在查询阶段的组装结构;已有 test_skills_do_not_grant_permissions() 检查申请说明不会在查询阶段开放准备工具,并拒绝未知名称。完整入口依赖共享环境中的框架和SQLite检查点集成;只观察加载本身,可以用仅依赖标准库的片段。
按 README 建好环境后,可在仓库根目录执行下面命令进入交互环境。-i 会先执行文件,再保留交互入口;脚本所在目录可用于导入 harness。这个观察不需要安装完整框架依赖,只需可用的 Python 3.12:
.\.venv-series\Scripts\python.exe -X utf8 -i examples/23-32-assistant/harness.py
然后执行下面片段。它调用的就是前面的实际函数,没有复制另一套加载实现:
from harness import assemble_request
allowed = {"policy_search", "order_query", "prepare_return", "submit"}
for name, stage in [("policy", "query"), ("return", "query"), ("return", "prepare")]:
request = assemble_request(name, stage, allowed, {"order_id": "A3003"})
print(name, stage, request["tools"])
print(request["instruction"].splitlines()[0])
print(assemble_request("return", "prepare", set())["tools"])
try:
assemble_request("../../secret", "query", allowed)
except ValueError as error:
print(str(error))
对应输出如下。工具名称按函数内的排序返回,不表示执行先后:
policy query ['policy_search']
# 政策解释
return query ['order_query']
# 申请准备
return prepare ['order_query', 'prepare_return']
# 申请准备
[]
未知说明名称
第一组换了说明,正文与工具需求一起变化;第二、三组保留同一申请说明,只改阶段,可用工具随之变化。授权为空时仍能读到登记的说明,但没有可用业务工具;未知名称则在读取文件前被拒绝。这里没有查询 A3003、创建草稿或提交申请,facts 中的订单号只是调用方提供的输入。
还可以把两份说明合并,比较全量字符数与选中正文字符数。字符数只是一种体积观察,不应冒充精确Token计数。真正计费与窗口预算需要按实际模型接口统计。
从“会员签收10天能否申请”到“为 A3003 准备草稿”,变化的是本轮目标、所需说明和数据。目录帮助找到说明,正文给出操作顺序,政策与订单提供事实,工具集合限定当前可提出的动作;实际业务仍由可信服务核对并产生结果。这样既能沿原来的退货流程继续处理,也能说明每轮输入为什么不同,而不会因为多读了一份说明就跳过确认或改写资格规则。
还没有公开评论
欢迎留下第一条想法,评论会在博主审核后显示。