标书智能体缓存优化:提示词顺序让成本直降10倍

2026-06-15阅读 0热度 0
智能体

开发AI应用时,多数团队都会纠结于提示词编写,却忽略了提示词顺序对成本的实际影响。

标书智能体(四)——提示词顺序优化,让缓存命中,输入成本直降10倍

开源代码已全部公开。

限于篇幅,本文仅展示核心代码与提示词片段,完整实现请查阅开源仓库。

GitHub仓库: https://github.com/FB208/yibiao-simple

Gitee仓库: https://gitee.com/yibiao-ai/yibiao-simple

本文聚焦第四期主题,剖析一个常被忽视却成本影响巨大的关键细节。

合理编排提示词顺序,不仅能提升输出质量,更能显著降低API调用成本。

前三期已完整搭建标书智能体的核心流程:

  1. 解析招标文件,抽取项目概述与技术评分细则。
  2. 基于解析结果自动生成技术标书目录大纲。
  3. 按大纲分章节逐段生成正文内容。

至此,系统已具备基本可用能力。

然而实际部署后会发现,标书任务与常规对话截然不同——成本大头不在模型输出,而在于输入上下文的反复传输。

一份招标文件动辄数万字,项目概述与评分细则篇幅可观。生成正文时,同一份项目概述、章节层级信息需反复提交给模型。

因此,成本优化的核心并非压缩提示词字数,而是重构提示词的排列顺序。

提示词结构若编排不当,服务商的缓存机制便难以生效,费用在无形中流失。

一、缓存为何难以命中

当前主流大模型服务商大多支持Prompt Cache或类似缓存机制。

但缓存并非简单的“内容相同即可”规则。

多数情况下,缓存机制依赖请求前缀的一致性。

即前缀部分是否稳定、一致,且每次请求都置于相同位置。

若将长篇正文置于提示词末尾,而前方拼接大量每次不同的说明,即使正文完全相同,服务商也难以将其识别为可复用前缀。

以文档解析功能为例。

针对同一份招标文件,需执行两次分析任务:

  1. 抽取项目概述
  2. 抽取技术评分要求

初始代码实现如下:

def build_analysis_messages(file_content: str, analysis_type: str) -> List[Dict[str, str]]:
    if analysis_type == 'overview':
        system_prompt = '系统提示:提取项目概述'
        analysis_type_cn = '项目概述'
    else:
        system_prompt = '系统提示:提取技术评分要求'
        analysis_type_cn = '技术评分要求'

    user_prompt = (
        f'请分析以下招标文件内容,提取{analysis_type_cn}信息:nn{file_content}'
    )

    return [
        {'role': 'system', 'content': system_prompt},
        {'role': 'user', 'content': user_prompt},
    ]

问题在哪里?

并非file_content不同——实际上file_content完全相同。

症结在于:

  1. 两次请求的system_prompt不同
  2. 两次请求的user_prompt前缀各异
  3. 消耗大量token的正文本file_content被置于末尾

于是服务商识别到的并非同一长前缀,而是两个不同请求后附带相同大段内容。

缓存命中率自然低效。

二、优化策略

理解问题后,解决方案直截了当。核心原则仅一条:

将体积大且稳定的上下文置于前端,将每次请求的任务差异移到末尾。

优化后的文档解析提示词结构如下:

def build_analysis_messages(file_content: str, analysis_type: str) -> List[Dict[str, str]]:
    system_prompt = """你是专业的招标文件分析助手。严格依据用户提供的招标文件原文完成分析。

通用规范:
1. 提取信息需全面准确,优先采用原文表述,不得杜撰
2. 仅输出最终分析结果,不附加任何说明、过程或客套
3. 若文档未提及某项内容,须明确标注“原文未提及”,不可自行补充
"""

    file_prompt = f"""以下是完整的招标文件全文,请先通读,并仅依据原文执行后续任务:

{file_content}"""

    if analysis_type == 'overview':
        task_prompt = '任务:提取并总结项目概述信息。...'
    else:
        task_prompt = '任务:提取技术评分要求。...'

    return [
        {'role': 'system', 'content': system_prompt},
        {'role': 'user', 'content': file_prompt},
        {'role': 'user', 'content': task_prompt},
    ]

改造后,两次请求的共同前缀变得明确:

  1. 第一条system消息完全相同
  2. 第二条全文user消息完全相同
  3. 仅最后一条任务说明存在差异

该结构相比“先写不同任务,再拼接同一份全文”的做法,更易触发缓存命中。

三、提纲生成环节的优化

第二期讨论提纲生成时,重点聚焦于JSON格式与目录质量。

从成本视角看,提纲生成同样存在类似的缓存问题。

同一项目内,用户常多次点击“重新生成目录”。

导致相同的overviewrequirements被反复提交给模型。

旧实现通常将它们拼接成一个庞大的user_prompt

user_prompt = f"""请根据以下项目信息生成标书目录:

项目概述:
{overview}

技术评分要求:
{requirements}

请输出完整的技术标目录,确保覆盖所有技术评分要点。"""

此写法本身无误,但缓存粒度不足。

现改为多消息结构:

def generate_outline_prompt(overview: str, requirements: str) -> List[Dict[str, str]]:
    return [
        {'role': 'system', 'content': _build_outline_system_prompt()},
        {'role': 'user', 'content': f'项目概述:n{overview}'},
        {'role': 'user', 'content': f'技术评分要求:n{requirements}'},
        {
            'role': 'user',
            'content': '请生成完整的技术标目录,确保覆盖所有技术评分要点。',
        },
    ]

若需基于用户已有目录进行扩写,同样拆分处理:

def generate_outline_with_old_prompt(
    overview: str,
    requirements: str,
    old_outline: str | None,
) -> List[Dict[str, str]]:
    return [
        {'role': 'system', 'content': _build_outline_system_prompt()},
        {'role': 'user', 'content': f'项目概述:n{overview}'},
        {'role': 'user', 'content': f'技术评分要求:n{requirements}'},
        {'role': 'user', 'content': f'用户已有目录:n{old_outline or ""}'},
        {
            'role': 'user',
            'content': '请在满足技术评分要求的前提下,充分利用用户提供的目录,生成完整的技术标目录。',
        },
    ]

如此编写的好处显而易见:

  1. 项目概述与评分要求成为稳定的共享上下文
  2. 普通目录生成与基于旧目录的扩写也能共享部分前缀

四、成本消耗最大的正文生成环节必须优化

第三期提及的正文生成已攻克两大核心问题:

  1. 标书篇幅过长,须拆分为叶子章节逐节生成
  2. 分节编写易产生重复,需同时传递上级章节与同级章节信息

此思路本身正确。

但从缓存优化角度回看,正文生成才是真正的成本重灾区。

因为正文生成是整个系统中调用频率最高的功能。

一个项目包含几十个叶子章节极为常见。每个章节发起一次请求,导致以下内容被反复传输:

  1. 同一份project_overview
  2. 相同的parent_chapters
  3. 高度重叠的sibling_chapters
  4. 完全一致的正文写作规范

旧写法将所有内容拼入一个庞大的user_prompt

user_prompt = f"""请为以下标书章节撰写具体内容:

{context_info}

当前章节信息:
章节ID: {chapter_id}
章节标题: {chapter_title}
章节描述: {chapter_description}

请依据项目概述及上述章节层级关系,生成详实的专业内容。"""

现改为分层消息结构:

def build_chapter_content_messages(
    chapter: Dict[str, Any],
    parent_chapters: List[Dict[str, Any]] | None = None,
    sibling_chapters: List[Dict[str, Any]] | None = None,
    project_overview: str = '',
) -> List[Dict[str, str]]:
    messages = [
        {'role': 'system', 'content': system_prompt},
    ]

    if project_overview.strip():
        messages.append(
            {'role': 'user', 'content': f'项目概述信息:n{project_overview}'}
        )

    if parent_chapters:
        messages.append({'role': 'user', 'content': parent_context})

    if sibling_chapters:
        messages.append({'role': 'user', 'content': sibling_context})

    messages.append(
        {
            'role': 'user',
            'content': f'''请为以下标书章节撰写具体内容:

当前章节信息:
章节ID: {chapter_id}
章节标题: {chapter_title}
章节描述: {chapter_description}

请根据项目概述及上述章节层级关系,生成详实的专业内容。''',
        }
    )

    return messages

此优化的价值极为显著。

同一父章节下的多个叶子节点通常具备以下共性:

  1. project_overview 相同
  2. parent_chapters 相同
  3. sibling_chapters 高度相似
  4. 实际变化最大的仅为最后一条当前章节请求

换言之,正文生成环节不仅请求数量多,且重复上下文长度极大。

这正是正文生成最值得进行缓存优化的原因。

五、经验总结

经反复测试验证,结论明确:核心不在于某段具体提示词,而在于以下这套规则。

今后但凡涉及高频AI任务,均可优先依此思路组织提示词:

1. system中仅放置稳定规则

切勿将本次请求的差异内容放入system。

system适合包含:

  1. 通用角色定义
  2. 通用写作规范
  3. 通用输出格式要求

2. 将最大且最稳定的上下文尽量前置

例如:

  1. 招标文件全文
  2. 项目概述
  3. 技术评分要求
  4. 目录结构
  5. 上级章节链

这些信息越稳定、越长、越可能被复用,就越应置于靠前位置。

3. 将任务差异尽量置于末尾

例如:

  1. 提取项目概述
  2. 提取技术评分要求
  3. 生成目录结构
  4. 生成第3.2.1节正文

这些都是每次请求中最易变动的内容,应置于消息序列末尾。

4. 保持同一份数据的组织格式稳定

缓存匹配的是前缀,而非仅“语义相近”。

因此以下细节必须保持一致:

  1. 标题格式统一
  2. 换行数量一致
  3. 列表顺序固定
  4. 避免时而strip()时而不strip()
  5. 杜绝将随机信息、时间戳注入共享上下文

这些细节看似微不足道,但对缓存命中率影响直接且显著。

六、实测效果

实测数据最具说服力。我们选用OpenRouter平台的gemini-2.5-flash模型,对一份数万字的招标文件进行解析测试,分别抽取项目概述与技术评分要求。结果清晰:首轮请求正常计费,第二轮请求的输入提示词基本命中缓存,成本降幅达约90%。

测试环境参数:

  1. 服务商:OpenRouter
  2. 模型:google/gemini-2.5-flash
  3. 请求端点:https://openrouter.ai/api/v1/chat/completions
  4. 测试内容:招标文件解析

首次请求日志关键字段:

request_id: c405ec7f755549629e8a47c04d5b2633
prompt_tokens:19349
cached_tokens: 0
upstream_inference_prompt_cost: 0.0058047

首次请求相当于写入缓存,故cached_tokens为0,输入费用按标准价格计算。

第二次请求日志关键字段:

request_id: b4cfd26bdef74b9c941263a96b692cea
prompt_tokens:19737
cached_tokens: 19442
upstream_inference_prompt_cost: 0.00067176

该结果直观验证了缓存优化的价值:

  1. 第二次请求成功命中缓存
  2. 缓存命中的token数高达19,442
  3. 输入费用从0.0058降至0.00067

换言之,同一份冗长的招标文件上下文,第二次请求的输入成本降至首次的约九分之一。此非理论推算,而是日志直接反映的数据。

这一点至关重要。它证明缓存优化绝非玄学或“可能节省一点”。只要满足以下条件:

  1. 前缀稳定
  2. 大文本足够长
  3. 第二次请求间隔足够短
  4. 模型与服务商支持缓存机制

节省的费用即可直接从日志中查证。

完整代码已开源

GitHub仓库: https://github.com/FB208/yibiao-simple

Gitee仓库: https://gitee.com/yibiao-ai/yibiao-simple

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策