ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

标书智能体提示词顺序优化实战:让 LLM 前缀缓存命中,输入成本直降 10 倍

标书智能体提示词顺序优化实战:让 LLM 前缀缓存命中,输入成本直降 10 倍 AI 应用AI 写作人工智能AI Agent企业应用【免费下载链接】OpenBidKit_Yibiao开箱即用的AI标书编写工具标书AI生成工具投标工具箱、知识库、标书查重、废标项检查完全开源免费欢迎使用项目地址https://gitcode.com/gh_mirrors/op/OpenBidKit_Yibiao点击查看免费下载在 AI 标书编写工具中一份几万字的招标文件会被反复送入模型解析项目概述、解析技术评分要求、生成目录、逐章生成正文。绝大多数团队把精力花在精简提示词上却忽略了一个更直接的成本杠杆——提示词的排列顺序。只要把稳定的大上下文前置、把每次请求的任务差异后置模型服务商的前缀缓存Prompt Cache就能持续命中输入成本可以下降近一个数量级。本篇以标书智能体OpenBidKit_Yibiao为实战背景完整讲解提示词顺序优化这一思路的来源、三类典型场景文档解析、提纲生成、正文生成的改造方案、四条可复用的组织原则并结合仓库源码验证这一思路在生产代码中的落地方式。读完本文你将掌握一套适用于任何高频 LLM 任务的前缀友好型提示词编排方法。一、为什么吃不到缓存缓存看的是前缀不是内容相同很多大模型服务商都支持 Prompt Cache或类似的输入缓存机制。但缓存命中并非只要内容一样就行在大多数实现里它匹配的是请求前缀——请求开头那一大段内容是否稳定、是否一致、是否每次都出现在同样的位置。反例很容易构造如果你把一份很长的正文放在提示词后面而前面先拼上了很多每次都不一样的说明比如任务名、时间戳、随机变量那么这份长文本虽然内容完全相同服务商也不会把它识别成同一个可复用前缀缓存自然无法命中。以文档解析功能为例对同一份招标文件系统需要做两次分析——提取项目概述、提取技术评分要求。最初的写法是典型的任务前置、大文本后置def build_analysis_messages(file_content: str, analysis_type: str) - List[Dict[str, str]]: if analysis_type overview: system_prompt 提取项目概述的 system prompt analysis_type_cn 项目概述 else: system_prompt 提取技术评分要求的 system prompt analysis_type_cn 技术评分要求 user_prompt ( f请分析以下招标文件内容提取{analysis_type_cn}信息\n\n{file_content} ) return [ {role: system, content: system_prompt}, {role: user, content: user_prompt}, ]问题不在file_content——恰恰相反两次请求的file_content完全相同。问题在于两次请求的system_prompt不一样两次请求的user_prompt前缀也不一样真正占 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}, ]改造后两个请求的共同前缀非常清晰第一条system完全一样第二条全文user完全一样只有最后一条任务说明不同。这种稳定前缀 尾部差异的结构比先写不同任务、再塞同一份全文的方式更容易命中缓存。仓库实证招标解析的前缀友好实现当前仓库的招标文件解析实现与上述思路完全同构核心代码在 bidAnalysisTask.cjs稳定的 system 提示词stableSystemPrompt第 12-18 行固定为投标资料分析助手 通用要求不随任务变化。大文本前置buildTenderContextMessages第 202-211 行把完整招标文件作为一条独立的user消息放在最前面function buildTenderContextMessages(fileContent, sectionHint) { const messages [ { role: system, content: stableSystemPrompt }, ]; if (sectionHint) { messages.push({ role: system, content: sectionHint }); } messages.push({ role: user, content: 以下是完整招标文件。后续任务需要基于这份招标文件完成如后续消息提供补充上下文请按具体任务要求综合使用\n\n${fileContent} }); return messages; }任务差异后置buildMessages第 213-219 行在此基础上再追加一条只包含当前任务说明的user消息也就是buildTaskPrompt(task)。任务本身也被拆成了稳定 JSON 模板 变化部分jsonTask(title, goals, outputJson)第 20-34 行把输出 JSON 的骨架固定下来只让 title/goals 变化保证同批任务间的格式前缀尽可能一致。更关键的是仓库还把缓存预热做成了显式流程runBidAnalysisTask第 437-450 行先串行执行一次项目概述解析随后waitForPromptCacheWarmup()等待PROMPT_CACHE_WARMUP_DELAY_MS 5000毫秒第 5-10 行日志里明确写着提示词缓存预热完成等待 5 秒后开始并发解析剩余项然后再用Promise.all并发执行其余十几个解析任务。这样第一批并发请求就能直接吃到第一条请求写入的前缀缓存。三、提纲编写部分优化多消息拆分替代单一大 prompt生成提纲时也有同样的问题。同一个项目里用户经常会多次点击重新生成目录于是同一份overview和requirements会被反复发给模型。旧写法通常是把它们拼成一个大user_promptuser_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: 请在满足技术评分要求的前提下充分结合用户自己编写的目录生成完整的技术标目录结构。, }, ]好处很直接项目概述和评分要求变成了稳定的共享上下文普通目录生成和旧目录扩写两种请求也能共享前面的长前缀。仓库实证目录生成的持久会话与工作区文件当前仓库的目录生成走的是持久 Agent 工作区文件路线任务标识定义在 outlineGenerationAgentV2Config.cjs流程在 outlineGenerationTaskV2.cjs。相比一次性把overview/requirements塞进单个 prompt它把项目概述、技术评分要求、目录树等材料写成工作区文件让模型在同一会话内反复读取重新生成目录时同一会话内已读取过的上下文天然具备前缀复用条件。这与稳定上下文尽量前置、可复用的原则殊途同归——一个在消息层面做前缀拆分一个在会话/工作区层面做上下文复用。四、消耗最大的正文编写必须优化正文生成是整个系统里调用次数最多的功能一个项目几十个叶子章节很正常每个章节都要发一次请求。于是以下内容会被反复传很多遍同一份project_overview相同的parent_chapters上级章节链高度重叠的sibling_chapters同级章节一模一样的正文写作规则。旧写法是把这些内容全拼进一个大user_promptuser_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这一步的价值特别大同一父章节下的多个叶子节点往往project_overview一样、parent_chapters一样、sibling_chapters高度接近真正变化最大的只是最后那条当前章节任务。正文生成不仅请求多而且重复上下文特别长所以它是最值得做缓存优化的环节。仓库实证正文生成的全轮公共前缀 并发预热当前仓库的正文生成实现contentGenerationAgent.cjs第 397-443 行把这一原则贯彻到了极致代码注释直接点明了设计意图// 全轮相同的规则和材料排在本节内容之前便于模型服务复用请求前缀缓存。 const system ${writingInstructions(...)}\n\n本次事实处理要求\n...; const sharedInput 项目概述 ${overview} 全局事实设定完整内容 ${facts} 字数要求 ${decisions.word_requirements} 用户额外要求 ${decisions.user_requirement} 受限 HTML 模板 ${template} 所选模板配置 ${config};system只装全轮不变的规则写作指令、全局事实处理要求、正文规则、配图类型对照表、配图要求、写作执行要求sharedInput只装全轮不变的输入材料项目概述、全局事实、字数要求、用户额外要求、受限 HTML 模板、模板配置每节差异压到最后一小段只有本节编排决策、本节配图安排与补充要求、补充参考资料摘录第 432-441 行跟着单条请求变化。并发控制也做了缓存配套contentGenerationAgent.cjs第 419 行在派发并发小节前调用warmSharedPrefix定义在 contentGenerationPrefixWarmup.cjs。该函数专门解决一个隐蔽问题——并发请求同时到达时彼此都还没来得及写入缓存谁都用不上谁。它的做法是// 并发请求同时到达时互相用不上缓存先用只含公共前缀的短请求写入缓存失败不影响后续并发。 async function warmSharedPrefix({ aiService, system, sharedInput, signal, onActivity, logTitle, label }) { onActivity?.({ message: 正在预热${label}缓存 }); try { await aiService.chat({ signal, logTitle, output_token_limit: 1, messages: [{ role: system, content: system }, { role: user, content: sharedInput }] }); // 部分服务在请求结束后才异步构建前缀缓存稍候再放开并发。 await delay(PREFIX_WARMUP_SETTLE_MS, undefined, { signal }); } catch (error) { signal.throwIfAborted(); onActivity?.({ message: ${label}缓存预热失败直接并发${error.message} }); } }预热请求只包含system sharedInput这段公共前缀并且把output_token_limit压到 1几乎不产生输出成本随后等待PREFIX_WARMUP_SETTLE_MS 1500毫秒让服务端完成缓存构建再放开并发。预热失败也不阻塞主流程直接进入并发。这套先小成本写缓存、再放大并发的策略是把前缀缓存从理论收益变成工程收益的关键一环。顺带一提仓库对缓存命中是有仪表盘的代理层 agentOpenAiProxy.cjs 的createPiSseUsage第 454-487 行会把服务商返回的cached_tokens归一化写入prompt_tokens_details.cached_tokens供上层用量统计与成本核算使用——这意味着缓存省下的钱不是玄学而是可以被量化观测的指标。五、总结四条可复用的提示词组织原则这次改造最有价值的不是某一段具体提示词而是下面这套规则。以后只要遇到高频 AI 任务都可以优先按这个思路组织提示词1.system只放稳定规则不要把这次请求特有的差异塞进system。system更适合放通用角色通用写作规范通用输出要求。2. 最大、最稳定的上下文尽量前置比如招标文件全文项目概述技术评分要求目录树上级章节链。这些内容越稳定、越长、越可能被重复利用就越应该尽量往前放。3. 任务差异尽量放最后比如提取项目概述提取技术评分要求生成目录生成 3.2.1 章节正文。这些都是每次请求最容易变化的内容应该尽量放到消息列表末尾。4. 同一份数据的组织格式要保持稳定缓存看的是前缀不只是意思差不多。所以这些细节都要保持一致标题写法一致换行数量一致列表顺序一致不要一会儿strip()一会儿不strip()不要把随机信息、时间戳塞进共享上下文。这些看起来都是小事但对缓存命中影响非常直接。六、实测结果输入成本直降 90% 以上原文档作者使用 OpenRouter 上的gemini-2.5-flash模型做了一次标书解析测试从几万字的招标文件中解析项目概述和技术评分要求。测试环境如下服务商OpenRouter模型google/gemini-2.5-flash请求地址https://openrouter.ai/api/v1/chat/completions测试方式招标文件解析。第一次请求写入缓存的关键日志字段request_id: c405ec7f755549629e8a47c04d5b2633 prompt_tokens19349 cached_tokens: 0 upstream_inference_prompt_cost: 0.0058047第一次请求相当于把缓存写进去所以cached_tokens为 0输入提示词费用按正常价格计算。第二次请求命中缓存的关键日志字段request_id: b4cfd26bdef74b9c941263a96b692cea prompt_tokens19737 cached_tokens: 19442 upstream_inference_prompt_cost: 0.00067176对比两次日志第二次请求已成功命中缓存命中的缓存 token 数达到19442输入提示词费用从0.0058047降到0.00067176。也就是说同样是一份很长的招标文件上下文第二次请求的输入成本降到了第一次的大约九分之一接近 10 倍差距。这证明了缓存优化不是玄学、不是可能会省一点——只要以下条件满足省下的钱可以直接从日志里看到前缀稳定大文本够长第二次请求跟得足够快模型和服务商本身支持缓存。七、延伸阅读本文讨论的核心代码与流程都可以在当前仓库中继续深挖bidAnalysisTask.cjs招标文件解析的稳定前缀、任务拆分与 5 秒缓存预热等待contentGenerationPrefixWarmup.cjs并发前的小成本前缀预热实现contentGenerationAgent.cjs正文生成全轮公共前缀 每节尾部差异的消息编排outlineGenerationTaskV2.cjs 与 outlineGenerationAgentV2Config.cjs目录生成的持久会话与工作区复用agentOpenAiProxy.cjscached_tokens的用量归一化用于观测缓存命中。提示词顺序优化是投入产出比极高的一类改造不改变模型、不改变输出质量、不需要换服务商只需要调整消息的排列结构和拼接格式就能让高频任务持续吃到前缀缓存。对于标书这类大上下文 高频重复请求的场景它带来的成本收益是直接且可量化的。赞分享AI 应用AI 写作人工智能AI Agent企业应用【免费下载链接】OpenBidKit_Yibiao开箱即用的AI标书编写工具标书AI生成工具投标工具箱、知识库、标书查重、废标项检查完全开源免费欢迎使用项目地址https://gitcode.com/gh_mirrors/op/OpenBidKit_Yibiao点击查看免费下载相关推荐Emacs-IPython-Notebook中的代码补全与调试提升Python开发体验Emacs IPython Notebook中的代码补全与调试提升Python开发体验 Emacs IPython NotebookEIN是一款强大的Ju10倍性能提升Nginx缓存命中率监控与优化实战指南10倍性能提升Nginx缓存命中率监控与优化实战指南 Nginx作为高性能的HTTP和反向代理服务器其缓存机制是提升网站性能的关键。本文将详细介绍如何监控和后端API网关负载均衡网络提升10倍性能Doctrine Annotations缓存优化实战提升10倍性能Doctrine Annotations缓存优化实战 你是否在开发大型PHP应用时遇到过启动缓慢的问题尤其是使用Doctrine ORM或Sy后端上一篇DLSS Swapper终极指南一键智能管理DLSS版本免费提升游戏性能45%下一篇N_m3u8DL-RE实战指南构建跨平台流媒体下载架构的最佳实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表