资讯动态

玩转 Spring AI 第三篇:自建 SearXNG 给大模型接上实时搜索——容器起来了,`format=json` 照样 403

发布时间:2026/10/19 12:03:39 来源:尧图企业网站定制
玩转 Spring AI 第三篇自建 SearXNG 给大模型接上实时搜索——容器起来了formatjson照样 403这是玩转 Spring AI系列的第三篇。前两篇聊的是 ChatClient 怎么把话接住、RAG 怎么把私有文档塞进上下文这一篇解决另一个问题模型不知道刚刚发生的事。笔者最初照着一份 Spring AI RAG MCP 实时搜索的课程笔记复现联网搜索那一段容器docker run一把过浏览器打开http://localhost:6080也正常搜索框里能出结果。于是很有信心地按笔记里的?q...formatjson试了一下接口——403 Forbidden。当时第一反应是镜像版本问题、是网络问题、是宿主机防火墙。实际原因是 SearXNG 上游配置里 JSON 输出默认就是关的*而这一步在整份讲义里一个字都没提。这类照着视频能跑、照着笔记必崩的缺口恰恰是自己动手时最花钱的地方。***这篇把这条链路完整拆开为什么要自建一层搜索、课程是怎么实现的、那个 403 的根因在哪一行源码里、代码本身有哪几处经不起推敲、前端那个联网搜索勾选框到底把请求路由去了哪。1. 后端接大模型为什么早晚要自己架一层搜索先把缺的是什么说清楚再谈选型。1.1 模型缺的从来不是智商是三样东西模型权重weights一旦训练完成模型对世界的认知就被冻结在那个时点。问它上周发布的某个版本是多少它要么答旧值要么答一个看起来很合理的编造值——这两种都比你想象的更难发现因为它语气一点问题都没有。各家具体的知识截止时间请以厂商模型卡为准本文不写死日期避免过期。缺口真实表现只改提示词能不能救知识截止时间新版本、新 CVE、新价格、新政策一律答不上或按旧知识自信作答不能。提示词改不出模型没见过的信息引用来源业务上要求这句话出自哪个链接模型只会给你一段没有出处的流畅文本半能。能要求它给出处但给不出真实出处成本为覆盖未知问题把大量资料塞进长上下文token 账单和延迟同步上涨反向恶化越补救越贵还有一类缺口是私有数据内部制度、工单、商品表模型完全不知道这条路只能靠外挂检索属于 RAG 的地盘本文第 2 节之后不展开。1.2 四条补信息的路成本不在一个量级这是笔者自己排过的一张表定性的相对成本不是报价路线数据新鲜度来源可溯前期改造量单次调用成本典型失效场景长上下文硬塞取决于塞进去的东西差模型不保证引用几乎为零最高全量文档每次重算文档一多就爆上下文、命中率反而下降RAG私有知识库 向量检索静态入库那一刻好片段能带回 metadata中切分 / Embedding / 向量库 / 入库管道中只塞回 Top K 片段回答不了库外的事也回答不了今天的事联网实时搜索本文实时中返回 URL 可展示但需二次校验低起一个搜索服务 拼 prompt中多一次 HTTP Top N 摘要搜索引擎本身被封、无结果、延迟不可控微调训练数据那一刻最差学进去就查不到出处最高数据构造 / 训练 / 评测 / 回滚低推理期无额外检索想改一条事实要重训一次时效类知识根本不该走这条路简单理解知识在库里 → RAG 知识在外面且一直变 → 联网搜索 行为/风格/领域话术 → 微调1.3 三个常见疑问Q1模型厂商不是自己有联网搜索吗为什么后端还要自己架一层因为后端要的不是会搜而是可控搜哪些引擎、返回几条、以什么字段结构进 prompt、失败怎么降级、请求出不出自己的网络边界。自建一层 SearXNG 这类元搜索Meta Search聚合多家引擎的开源搜索聚合器之后这条链路整个在你手里搜索引擎聚合、翻页、语言、安全过滤都在一个settings.yml里。这一条属于笔者工程判断不是对任何厂商功能的评价。Q2已经有 RAG 了联网搜索算不算重复建设不算两者检索的是两个不相交的集合。这套课程里两条链路的代码形状甚至几乎一样——都是检索 → 拼{context}→ 交给 Spring AI 的 ChatClient区别只在检索源RAG 走redisVectorStore.similaritySearch(question)联网走sesrXngService.search(question)。私有知识进库公开时效进搜索这是很自然的分工。Q3自建搜索层最贵的是什么不是钱是**“它坏了你不知道”**。多出来的这段链路是纯外部依赖容器状态、上游引擎可用性、返回结构变更、超时。工程上这段的默认失败模式是静默降级成没搜索——模型照旧很流畅地答一段过时内容。所以本文第 3、4 节花最多篇幅的就是失败路径。2. 这套课程的联网搜索链路七段每段都能单独坏先给全景再逐段对代码。docker run searxng/searxng → http://host:6080/search?qformatjson → OkHttp 请求 → JSON 字符串 → hutool JSONUtil.toBean → ListSearchResult → dealResults 截断/排序 → context[来源]...[摘要].../context → sesrXngPROMPT.replace → Prompt → chatClient.prompt(prompt).stream().content() → SSE 逐 token 推给前端2.1 起容器讲义里那条docker run课程用的是官方镜像searxng/searxngDocker Hub 镜像页Docker 本体官网见参考资料。讲义原文去掉个人路径后是这样dockerpull searxng/searxng:latestdockerrun-p6080:8080--namesearxng-d--restartalways\-v挂载根目录/SearXNG:/etc/searxng\-eBASE_URLhttp://localhost:$PORT/\-eINSTANCE_NAME实例名\searxng/searxng参数作用值得注意的点-p 6080:8080容器内 SearXNG 监听8080宿主映射到6080官方 Docker 文档的示例用的也是容器侧 8080它示例映射到宿主 8888端口号是约定不是硬性要求-v ...:/etc/searxng把配置目录挂出来settings.yml就在这里这是本篇第 3 节能被修好的唯一前提。没挂这个目录你连改配置的地方都没有-e BASE_URL...实例对外基础 URL影响分页/回调等绝对链接讲义这行有个坑$PORT在 shell 里从未定义展开成空串实际值是http://localhost:/。这个坑在浏览器里几乎看不出来但拼出来的绝对 URL 是坏的-e INSTANCE_NAME...实例名页面标题等展示用途官方文档把BASE_URL/INSTANCE_NAME归到 server / general 配置并说明$SEARXNG_*环境变量可覆盖配置项--restartalways宿主机重启后自动拉起云服务器那章同一条命令只换了挂载根目录两个小事实顺便记一下讲义docker pull带了:latest但docker run不带 tag——两条命令引用的其实是同一个默认 tag但镜像没有锁版本。另外官方文档的示例还多挂了一个-v ./data/:/var/cache/searxng/缓存目录讲义没有容器一删缓存就没了。2.2 后端配置项internet:websearch:searxng:url:http://localhost:6080/searchcounts:10url直接指到/search端点counts是进入 prompt 的结果条数。Java 侧字段名是大写的SEARXNG_URL/COUNTS常量风格命名实际是实例字段靠Value注入。2.3 发起请求一个全局 OkHttpClientOKHttpConfig里只有一段OkHttp 官网见参考资料BeanpublicOkHttpClientokHttpClient(){returnnewOkHttpClient.Builder().connectTimeout(30,TimeUnit.SECONDS).readTimeout(60,TimeUnit.SECONDS).build();}服务实现侧的调用骨架按工程事实还原非逐字抄录HttpUrlurlHttpUrl.get(SEARXNG_URL).newBuilder().addQueryParameter(q,query).addQueryParameter(format,json)// 关键就是这一个参数触发第 3 节的 403.build();RequestrequestnewRequest.Builder().url(url).build();try(ResponseresponseokHttpClient.newCall(request).execute()){if(!response.isSuccessful()){thrownewRuntimeException(请求失败: HTTP response.code());}StringresponseBody/* response.body() 读出字符串 */;SearXNGResponsebeanJSONUtil.toBean(responseBody,SearXNGResponse.class);returndealResults(bean.getResults());}// IOException → new RuntimeException(e)方法末尾兜底 return Collections.emptyList();参数拼装走的是 OkHttp 的HttpUrl.newBuilder()而不是字符串拼接这一点是对的查询词里带中文、空格、时会被正确转义。但 30s / 60s 这两个超时值得说一句。这是一条用户正在等首 token的交互式链路readTimeout60 秒意味着搜索卡住时前端可以整整一分钟收不到任何响应而工程里没有任何重试或熔断。笔者会把它压到连接 35s、读 810s超时即降级为无搜索结果 明确话术这是笔者判断不是课程设定。2.4 反序列化只映射了两个字段SearXNGResponse和SearchResult都是 Hutool 的JSONUtil.toBean(...)直接映射字段严格照代码类字段类型SearXNGResponsequeryStringSearXNGResponseresultsListSearchResultSearchResulttitle/url/contentStringSearchResultscoredoubleSearXNG 的 JSON 响应里还有answers、corrections、number_of_results、response_time、unresponsive_engines、infinite_scroll这些字段工程一个都没建模。其中unresponsive_engines哪些上游引擎这次没响应和response_time恰恰是排障和可观测最该看的两项它们能告诉你这次结果少是因为上游挂了一半。丢弃它们意味着搜索质量退化在系统里是完全不可见的这条批评属笔者判断字段本身在工程里未建模是事实。2.5 结果处理dealResultsresults.subList(0,Math.min(COUNTS,results.size())).parallelStream().sorted(Comparator.comparingDouble(SearchResult::getScore).reversed()).limit(COUNTS).toList();四步截断 → 并行流 → 按score降序 → 再limit。这里埋着一个语义问题单独放到第 4 节讲。2.6 拼上下文与提示词ChatServiceImpl.buildSesrXngPrompt把每条结果格式化成context [来源] {url} [摘要] {content} /context注意它只用了url和contenttitle和score被完全丢弃。用户看到的引用是一串裸 URL没有标题可读性和可核验性都差一截——这是本节里成本最低、收益最高的一处改动。模板全文工程里的sesrXngPROMPT你是一个互联网搜索大师请基于以下互联网返回的结果作为上下文根据你的理解结合用户的 提问综合后生成并且输出专业的回答【上下文】{context}【问题】{question}【输出】 如果没有查到请回复不知道。如果查到请回复具体的内容。和 RAG 那段ragPROMPT相比只差两处角色描述不同、RAG 多一句不相关的近似内容不必提到。这说明课程里联网和知识库在实现上就是同一套手拼 prompt 模板换了个检索源。2.7 交给 Spring AI 流式输出PromptpromptnewPrompt(finalPrompt);FluxStringstringFluxchatClient.prompt(prompt).stream().content();后面和另外两条链路完全汇合Flux经toStream()阻塞式逐 token 消费每段内容以 SSE 的add事件推给前端聚合完再发一条finish。2.8 总归来说这条链路没有用任何 Spring AI 的搜索抽象就是手写 HTTP 手拼 prompt。好处是每一步都能自己 debug坏处是每一步都得自己兜。七个环节里只有第 6、7 段是 Spring AI 的前面五段全是自己搭的胶水——而本篇的坑恰好全在胶水里。3. 最关键的一节容器起来了formatjson还是 403这一节是我复现时卡最久的一段也是整份讲义最大的一处缺失。3.1 现象# 浏览器打开一切正常http://localhost:6080/?q关键词# 换成 JSON 输出立刻 403curl-ihttp://localhost:6080/search?q关键词formatjson# HTTP/1.1 403 FORBIDDEN网页能用接口不能用。这个组合很容易把人带偏因为 403 通常意味着权限/鉴权/防火墙而这里既没有 token 也没有 ACL——它其实是在说这个输出格式在本实例的配置里不存在。3.2 根因上游默认只开 html笔者于 2026-09-29 拉取 SearXNG 上游仓库searxng/searxng的 master 分支核对两处源码直接给出答案上游仓库。searx/settings.yml约:83-85同一段在官方文档页里也原样呈现search:# remove format to deny access, use lower case.# formats: [html, csv, json, rss]formats:-htmlsearx/webapp.py笔者记录约:630-631ifoutput_formatnotinsettings[search][formats]:flask.abort(403)翻译成人话formats是一份输出格式白名单默认列表里只有html请求里带的formatjson落不进白名单直接flask.abort(403)。那两行注释其实已经把语义写明白了——“remove format to deny access”不在列表里就是拒绝访问而且要求小写。项事实默认值search.formats: [html]upstream master2026-09-29 核对可选值html/csv/json/rss校验位置webapp.py里对output_format做白名单判断不在名单即abort(403)网页为何正常html本来就在默认白名单里行号随分支版本漂移别把行号当依据以内容为准3.3 修复在挂载出来的settings.yml里加一行search:formats:-html-json# ← 没有这一行Java 侧永远拿 403改的是挂载进容器的那个目录里的settings.yml对应本例-v 挂载根目录/SearXNG:/etc/searxng。第一次启动时该目录可能没有这个文件需要让容器生成或从上游模板拷一份进去这一步是笔者经验官方文档只说明配置文件位于挂载出来的settings.yml。改完要重启容器让配置重新加载——同样属笔者经验非官方原文。dockerrestart searxngcurl-shttp://localhost:6080/search?q关键词formatjson|head-c400看到query和results两个键链路才真正通了。3.4 顺带两个同样看着没事、其实坏了的配置点BASE_URL里的$PORT未定义。同一条docker run里写-e BASE_URLhttp://localhost:$PORT/shell 展开成空串。浏览器主页面不受影响但由实例拼出的绝对链接分页等会带上一个畸形主机串。落地时应写成明确的http://你的地址:6080/。-p 6080:8080 开了json 一台对全网开放的搜索代理。讲义后续把同样的容器直接起在云服务器上公网地址这里不复现。一旦json白名单打开、又没有限流与访问控制任何能连到 6080 的人都可以拿你的实例刷上游引擎——被封的是你的出口 IP。合理的做法是只允许后端访问内网 / 安全组 / 反向代理加认证。关于 SearXNG 官方 limiter 对该场景的具体配置项本文未逐条核实上面这条是笔者工程判断。3.5 这一点讲义里有没有没有。课程第 7 章那份使用 Docker 安装 SearXNG讲义只有docker pull、上面那条docker run、访问http://localhost:6080、以及一条 API 示例http://localhost:6080/search?q关键词formatjson第 7 章在项目中集成 SearXNG那份讲义只给了 okhttp 依赖和一行 URL第 10 章云服务器那节是同一条命令换个挂载路径同样直接给了带formatjson的 API 示例。三份讲义从头到尾没有出现settings.yml、没有出现formats。这类缺失的杀伤力在于视频里老师能跑通是因为他的容器早就配过而学员照笔记新建容器跑通的只有网页那一步。工程侧表现为搜索服务起来了接口一直 403代码看起来完全没问题。笔记复现型项目里最贵的从来不是难的功能是只在演示者机器上成立的那一步。4. 代码层面的三处真实瑕疵这三条都不是风格问题是会改变结果的。4.1dealResults先截断再排序// 现在的顺序subList(0,min(COUNTS,size))→sorted(scoreDESC)→limit(COUNTS)subList已经先把列表切成了原始顺序的前COUNTS条之后那次sorted只是在这一小撮里重新排。而 SearXNG 返回的results本身有它自己的排序所以这段代码的真实语义是在原始结果的前 10 条里按 score 排一遍 而不是 在所有结果里取 score 最高的 10 条设定上游返回期望结果实际结果counts 1020 条第 12 条score全场最高第 12 条进入上下文第 12 条在第 09 条截断时就被丢掉永远进不了 prompt由于两次操作条数上限相同subList的COUNTS与limit(COUNTS)最后那次limit实际上是个空操作。正确写法只有一行顺序调整——先排序再截断results.stream().sorted(Comparator.comparingDouble(SearchResult::getScore).reversed()).limit(COUNTS).toList();顺带一句这里用parallelStream()给 10 条数据做排序除了多起线程池任务之外没有任何收益属于随手写的。4.2 null 与异常路径一个都不拦现状事实search()里!response.isSuccessful()会throw new RuntimeException(请求失败: HTTP code)IOException也包成RuntimeException抛出方法末尾有一条兜底return Collections.emptyList()。问题分三层buildSesrXngPrompt对入参零防护。它直接对列表forEach一旦search()返回null这里立刻 NPE。现有代码里的 null 从哪来需要具体条件——例如上游返回体不带results键时JSONUtil.toBean会把该字段留为nulldealResults第一行results.subList(...)就先炸这条链路是笔者推演未实测。但调用方没有任何 null / isEmpty 判断这一点本身看代码就能确认。异常直接冒到 Controller工程里没有全局异常处理器——整个仓库没有ControllerAdvice/ExceptionHandler事实。统一返回体LeeResult定义了 500/501/555 等状态码但在联网这条路上没有任何地方把失败转成LeeResult前端拿到的是裸 500而不是一个可读的搜索失败。空列表会静默拼出一个空{context}。搜索没结果时prompt 里的【上下文】位置是空的模型只能靠如果没有查到请回复不知道这一句自己判断。它到底会不会老实地说不知道取决于模型而不是取决于代码——工程上这里必须有显式的分支无结果 → 明确话术 / 换关键词重试 / 回落普通对话。这一条是笔者判断课程没有演示过失败分支。4.3score的语义从讲义到代码没人交代SearchResult.score是double排序和Comparator.comparingDouble都建立在一个假设上这个值越大越相关且可比。但待确认项本文结论不同引擎分组的原始分数量级是否一致未核实score是否已归一化、取值范围是什么未核实讲义从未说明是否存在负分 / 同分 / 缺省 0未核实这条之所以要单独列出来是因为它直接决定后续能不能做阈值过滤score X 就不进 prompt。在没有实测过score分布之前任何相关度阈值都是拍脑袋。想加这层过滤的话第一步是把几百条真实响应的score打出来看直方图而不是找一篇博客抄个数字。4.4 三条低成本改进改动成本收益排序与截断换顺序一行真正拿到 top-Nsearch()与buildSesrXngPrompt之间加 null/empty 分支 全局异常兜底小失败可观测、可降级上下文里补上title并保留unresponsive_engines/response_time到日志小引用可读、搜索质量退化可见5. 前端那个联网搜索勾选框到底把请求路由去了哪后端做完了还得看它是怎么被选中的。课程前端是 Vue 2 Element UI 2.15.13脚本直接script引本地库、无构建。页面上有两个开关internetSearchSelected联网搜索与knowledgeSearchSelected知识库搜索发送时是三分支互斥路由仅知识库搜索勾选 → POST /rag/search → similaritySearch → ragPROMPT 仅联网搜索勾选 → POST /internet/search → sesrXngService.search → sesrXngPROMPT 两者都不选 → POST /chat/doChat → 无检索直接对话勾选态前端方法接口上下文来源联网 ON / 知识库 OFFdoctorApi.internetSearchPOST /internet/searchSearXNG Top N 摘要联网 OFF / 知识库 ONdoctorApi.ragSearchPOST /rag/searchRedis 向量库命中片段两个都 OFFdoctorApi.doChatPOST /chat/doChat无只有会话记忆两个都 ON不存在这种路由—两个开关在点击处理里互相关闭三条链路在后端最终都收敛到同一个形状chatClient.prompt(prompt).stream().content()→ SSEadd逐 token → 聚合后finish一次性给全文前端用marked.parse把整段 Markdown 覆盖渲染。所以前端要同时处理逐字追加和最终整段替换两种渲染这是这类 SSE 桥接最容易被忽略的实现细节。Q1为什么要让两个开关互斥而不是同时开因为课程的实现是一个 prompt 模板对应一个检索源。要同时开就得把向量片段和网络摘要拼进同一个{context}并处理两者的相对顺序与总长度——那是另一套代码。互斥是一个成本很低的取舍先把两条通道分别跑通。代价是私有资料 最新公开信息这类真实复合问题答不了而这恰恰是企业场景里最常见的形态。Q2前端勾选状态能算安全边界吗不能。它只决定这次调哪个接口不决定你有没有权限调。这三个接口本身没有任何鉴权任何人绕开页面直接POST /internet/search一样能触发一次搜索 一次大模型调用——这就是一个花钱的开放接口。落地时这类接口必须在服务端做身份与配额校验笔者判断课程未做。Q3这条链路还有哪些前端/后端错配工程事实里有两处可以顺手记下来接口封装里有一个getRecords → GET /ollama/getRecords而后端根本没有这个 Controller主页面监听的事件名是customEvent驼峰后端发出的事件名是custom_event下划线另两个演示 SSE 页用的才是custom_event。这类前端先行写了接口名、后端章节还没跟上的残留在跟着视频敲的项目里很常见值得在复现时先全局搜一遍。6. 如果是我来落地可控联网搜索的核对清单把前面所有坑压成可执行的检查项。笔者后端 架构的联网搜索落地策略层必做项依据来自本文哪一节部署挂载/etc/searxng镜像 tag 锁到具体版本不用latest缓存目录单独挂§2.1配置settings.yml的search.formats显式含jsonBASE_URL写死成完整可访问地址§3.2 / §3.4暴露面6080 不直连公网只允许后端访问需要对外时加认证与限流§3.4调用连接/读超时按交互链路收紧秒级加一次重试与熔断超时也要打点§2.3解析把unresponsive_engines、response_time、number_of_results落日志results缺失时视为失败而不是 NPE§2.4 / §4.2排序先排序后截断score阈值必须先实测分布再启用§4.1 / §4.3提示词上下文带标题与来源区分搜索失败 / 无结果 / 有结果三种话术§2.6 / §4.2降级搜索不可用时明确回落普通对话并在回答里声明本次未联网§4.2可观测每次搜索记录耗时、结果条数、HTTP 状态码403/超时/0 结果单独告警§3.1 / §4.2接口/internet/search前置鉴权与配额§5 Q2Q课程锁 spring-ai-bom 1.0.0现在应该用哪个版本工程里spring-ai-bom是1.0.0父 POM 之下由 Spring Boot 3.5.0 管理版本。笔者 2026-09-29 通过 GitHub API 核了官方仓库的 releases版本发布日期v1.0.9/v1.1.8/v2.0.02026-06-12v2.0.12026-08-21v2.1.0-M12026-09-25也就是说课程快照落后当前稳定版若干个小版本并且已经有 2.x 线。这里给两条建议笔者判断一是照课程复现时不要顺手升 BOM1.0.0 → 2.x 属于大版本跨度MCP 与 vector-store 的 starter 坐标和自动配置项都在动学习期换版本等于同时改两个变量二是新项目按稳定线选并锁定具体小版本-M1这类里程碑版本不进生产。每个版本具体变更了什么本文未逐条核对 release notes请以官方发布说明为准。Q联网搜索该不该做成模型可以自己调用的工具课程这条路是前端开关决定要不要搜后端固定搜一次模型自己不参与决策优点是可预测、可缓存、账单可控。另一种做法是把搜索注册成一个 tool由模型判断这个问题需不需要查体验更顺代价是多轮推理 不确定的搜索次数。就这类工程笔者的顺序是先把固定检索做稳超时、403、排序、降级都收干净再谈工具化。课程另一侧已经有完整的 MCP 工具链DateTool/EmailTool/ProductTool把 SearXNG 包成一个搜索 MCP tool 是这套工程最自然的下一步——这是笔者推断不是课程已有实现。最后总结联网搜索不是给模型加个功能而是给系统加一段外部依赖。它带来的三类问题里只有数据新鲜度是需求来源可溯和失败可见才是工程。本篇最重要的一行是formats: - json。SearXNG 上游默认白名单只有html未启用的输出格式在webapp.py里直接flask.abort(403)容器能开、网页能用、接口 403这三件事同时成立是正常的。三份讲义都没有这一步属于典型照着视频能跑、照着笔记必崩。dealResults先截断后排序不是风格问题而是语义问题它实现的是在前 N 条里排序不是取全局最相关 N 条高分结果会被无声丢掉交换顺序即可。null 与异常路径全程裸奔buildSesrXngPrompt对入参没有防护工程里没有全局异常处理器搜索失败会以 500 或 NPE 的形态出现而不是一个可读的降级话术。score是否归一化讲义和代码都没说本文也未核实。想做相关度阈值先实测分布。前端两个开关互斥是简单实现换来的取舍省掉了多源上下文拼装代价是私有资料 实时信息这类复合问题答不了而这三个接口目前没有任何服务端鉴权。对后端/架构岗笔者个人最看重这三条6080有没有暴露到公网超时是不是 30s/60s 这种交互链路不该出现的数字unresponsive_engines这类质量信号有没有被丢掉。版本上课程锁的spring-ai-bom是 1.0.0官方已有 1.0.9 / 1.1.8 / 2.0.x 等 releases2026-09-29 核对。复现教学项目别顺手升大版本。下一篇讲把这套工程里的工具能力彻底搬出去MCP server 的 stdio 与 SSE 两种传输、工具描述怎么写才不会让模型瞎填参数以及部署到云服务器时 Nginx 对 SSE 长连接到底缺了哪些配置。事实说明本文关于课程工程的类名、配置项、代码片段、前端路由与讲义缺失均来自 2026-09-29 对该仓库与 21 篇讲义的只读实读凭据已脱敏服务器公网地址与讲义中的个人姓名 / 关键词未复现调用骨架按事实还原、非逐字抄录已就地标注。关于 SearXNG 的部分为upstream 已核实searx/settings.yml的search.formats默认仅html、可选html/csv/json/rss、searx/webapp.py对未启用格式flask.abort(403)2026-09-29 拉取 upstream master 核对文中行号随版本漂移不作依据官方 Docker 文档示例的容器端口 8080、/etc/searxng配置卷与BASE_URL/INSTANCE_NAME/$SEARXNG_*亦已核实。Spring AI releases 表来自 2026-09-29 GitHub API。以下均未核实正文已标注且不作断言score是否归一化及其取值范围results字段缺失导致 NPE 的具体触发链笔者推演配置修改需重启容器生效、挂载目录首次为空时的配置文件生成、SearXNG limiter 针对 JSON/CSV 的具体配置项ChatMemoryBean 来源及各版本 release notes 明细。“四条补信息路线的成本对比”“超时应收紧”“互斥路由是取舍”“先做固定检索再谈工具化”6080 不该裸奔公网等为笔者工程判断非官方或课程表述。参考资料 致谢[1] SearXNG 官方文档设置与安装入口[2] SearXNG 文档Search settingssearch.formats默认与可选格式[3] SearXNG 文档Docker 安装容器侧 8080 端口、/etc/searxng配置卷、SEARXNG_*环境变量[4] SearXNG 上游仓库searx/settings.yml、searx/webapp.py2026-09-29 master 核对[5] SearXNG 官方 Docker 镜像 searxng/searxng[6] Spring AI 项目主页[7] Spring AI Reference参考文档[8] Spring AI GitHub Releases版本与日期依据[9] Spring Boot 项目主页[10] OkHttp 官方文档[11] Hutool 官方文档[12] Docker 官网[13] DeepSeek 开放平台课程默认模型[14] Redis 官网课程 RAG 向量库对照[15] Vue.js 官方文档[16] Element UI 仓库[17] 本课程工程与 21 篇讲义Spring AI RAG MCP 实时搜索仅作为本文事实来源凭据与个人信息已全部脱敏文中类名、配置项、行位置与缺失清单均据此核对。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑