<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ice的空间]]></title><description><![CDATA[哈喽~]]></description><link>https://dryice.icu</link><image><url>https://dryice.icu/innei.svg</url><title>ice的空间</title><link>https://dryice.icu</link></image><generator>Shiro (https://github.com/Innei/Shiro)</generator><lastBuildDate>Thu, 13 Aug 2026 17:51:15 GMT</lastBuildDate><atom:link href="https://dryice.icu/feed" rel="self" type="application/rss+xml"/><pubDate>Thu, 13 Aug 2026 17:51:15 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[我如何使用 Claude 的每一个代码功能]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.dryice.icu/images/2026/02/26/1772066340153_context.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/02/26/1772066341464_models.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://dryice.icu/posts/share/claude-code">https://dryice.icu/posts/share/claude-code</a></blockquote><div><h2 id="claudemd">CLAUDE.md</h2><p>在代码库里，要想高效使用 Claude Code，最重要的文件就是根目录下的CLAUDE.md。它是Agent的行为准则，是了解你这仓库运作方式的首要依据。</p><p>如何对待这个文件，要看具体场景。对于我的兴趣项目，我让Claude 想写什么就写什么。</p><p>在我的工作上，公司仓库Monorepo中的<code>CLAUDE.md</code>维护得非常严格，目前大小约为13KB（完全有可能增长到 25KB）。</p><p>• 它只记录大多30%（这个阈值比较随意）的工程师会用到的工具和 API（其他工具会在产品或库专属的Markdown文件里记录）。</p><p>• 我们甚至开始每个内部工具的文档分配最大token 数。 如果你不能简明扼要地解释你的工具，那它就还没准备好被放进 <code>CLAUDE.md</code>。</p><h2 id="">技巧与常见反模式</h2><p>随着时间推移，我们形成了一套鲜明且有主见的写作哲学，来打造高效的<code>CLAUDE.md</code>。</p><ol start="1"><li><strong>先设限制，而不是写指南。</strong> 你的 <code>CLAUDE.md</code> 应从小处着手，根据 Claude 常犯的错误来逐步记录相关内容。</li><li><strong>别在<code>CLAUDE.md</code>里到处 @ 引用文档。</strong> 如果你在别处已有大量文档，很容易想在CLAUDE.md里@这些文件。这会在每次运行时把整份文件塞进上下文窗口，导致臃肿。但如果你只是在文中提到路径，Claude 通常会忽略它。相反的，你必须向 Agent 推销 <strong>“为什么”</strong> 以及 <strong>“什么时候”</strong> 需要读这份文件：<strong>“遇到复杂用法或碰到 FooBarError 时，请参见 path/to/docs.md 获取最佳问题排查步骤。”</strong></li><li><strong>不要只说“禁止”。</strong> 避免纯粹的负面约束，例如 <strong>“绝对不要使用 --foo-bar 标志”</strong><br/> 当Agent认为它必须使用该标志时，就会左右脑互博，导致卡住。所以永远要提供可行的替代方案。</li><li><strong>把 CLAUDE.md 当成强制性手段。</strong> 如果你的CLI 命令复杂又冗长，与其写长篇大论解释它们，不如写一个简单的bash 包装器，提供清晰直观的 API，然后记录这个包装器。保持<code>CLAUDE.md</code> 尽可能短，是迫使你精简代码库和内部工具的绝佳手段。</li></ol><p>这是一个简化的示例</p><pre class=""><code class=""># Monorepo

## Python
- 总是...
- 使用 &lt;command&gt; 进行测试
... 还有10条 ...

## &lt;内部 CLI 工具&gt;
... 10个要点，聚焦于80%的使用场景 ...
- &lt;使用示例&gt;
- 总是...
- 禁止 &lt;x&gt;，优先使用 &lt;Y&gt;

对于 &lt;复杂用法&gt; 或 &lt;错误&gt;，请参阅 path/to/&lt;tool&gt;_docs.md
...
</code></pre><p>最后，我们会将这个文件与一个 <code>AGENTS.md</code> 文件保持同步，以确保与其他我们工程师可能在使用的 AI IDE 兼容。</p><blockquote><p><strong>核心要点：</strong></p></blockquote>
<p>把 <code>CLAUDE.md</code> 当作一套<strong>高层次、精心策划的护栏和指引</strong>。用它来指导你在哪里需要投入更多精力来打造对 AI（和人类）更友好的工具，而不是试图把它变成一本无所不包的百科全书。</p><p><em>如果您正在寻找更多关于为编码助手编写 Markdown 的技巧，请参阅 <a href="https://blog.sshh.io/p/ai-cant-read-your-docs">“AI 无法读懂你的文档”</a><a href="https://blog.sshh.io/p/ai-powered-software-engineering">、“AI 驱动的软件工程”</a>,以及 <a href="https://blog.sshh.io/p/how-cursor-ai-ide-works">“Cursor（AI IDE）的工作原理”</a>.</em></p>
<h2 id="">上下文管理之压缩和清理</h2><p>我建议在编码会话中至少运行一次 <code>/context</code> ，来了解你那 200k token 的上下文窗口是如何被使用的（即使是 Sonnet-1M，我也不相信完整的上下文窗口能被有效利用）。对我们来说，在我们的 monorepo 中，一个全新的会话基线成本大约是 20k token (10%)，剩下的 180k 用于你的实际修改——而这很快就会被填满。</p><p><img src="https://img.dryice.icu/images/2026/02/26/1772066340153_context.png" alt="img" height="720" width="1158"/></p><p><em>这是我最近一个个人项目中 <strong>/context</strong> 的截图。你可以把它想象成磁盘空间，随着你开发一个功能，它会逐渐被填满。几分钟或几小时后，你就需要清除消息（紫色部分）来腾出空间继续工作。</em></p>
<p><img src="https://img.dryice.icu/images/2026/02/26/1772066341464_models.png" alt="img"/></p><p><em><strong>「Ads」CCometixLine</strong> 超绝观测 <strong>Context Window</strong></em></p>
<p>三个主要的工作流程：</p><ol start="1"><li><strong>/compact (避免使用)</strong> 我尽可能避免使用这个命令。它的自动压缩过程不透明、容易出错，而且优化得不好。</li><li><strong>/clear + /catchup (简单重启)</strong> 这是我的默认重启方式。我用 <code>/clear</code> 清除状态，然后运行一个自定义的 <code>/catchup</code> 命令，让 Claude 读取我当前 git 分支中所有已更改的文件。</li><li><strong>“记录并清除” (复杂重启)</strong> 用于大型任务。我让 Claude 把它的计划和进展输出到一个 <code>.md</code> 文件中，然后用 <code>/clear</code> 清除上下文，接着通过让它读取那个 <code>.md</code> 文件来开始一个新的会话并继续工作。</li></ol><p><strong>核心要点：</strong></p><p>不要相信自动压缩。对简单的重启使用 <code>/clear</code>，对复杂任务使用“记录并清除”的方法来创建持久的外部“记忆”。</p>
<h2 id="">自定义斜杠命令</h2><p>我把斜杠命令看作是常用提示的简单快捷方式，仅此而已。我的配置非常精简：</p><ul><li><strong>/catchup</strong> : 就是我前面提到的命令。它只是提示 Claude 读取我当前 git 分支中所有已更改的文件。</li><li><strong>/pr</strong> : 一个简单的辅助工具，用来清理我的代码、暂存更改，并准备一个 Pull Request。</li></ul><p>恕我直言，如果你有一长串复杂的自定义斜杠命令，你就制造了一个反模式。对我来说，像 Claude 这样的Agent的全部意义就在于，你可以输入<strong>几乎任何</strong> 你想要的东西，并得到一个有用的、可合并的结果。一旦你强迫一个工程师（或非工程师）为了完成工作而去学习一个需要查文档的、新的魔法命令列表，你就失败了。</p><blockquote>
<p><strong>核心要点：</strong></p></blockquote>
<p>把斜杠命令当作<strong>简单、个人化</strong>的快捷方式，而不是用它来替代构建更直观的 <code>CLAUDE.md</code> 和更好的工具化Agent。</p>
<h2 id="subagent">自定义SubAgent</h2><p>理论上，SubAgent是 <code>Claude Code</code> 在上下文管理方面最强大的功能。它的理念很简单：一个复杂任务需要 <code>X</code> token 的输入上下文（例如，如何运行测试），在工作过程中累积了 <code>Y</code> token 的上下文，并产出一个 <code>Z</code> token 的答案。运行 <code>N</code> 个这样的任务意味着你的主窗口中会有 <code>(X + Y + Z) * N</code> 个 token。</p><p>SubAgent的解决方案是，将 <code>(X + Y) * N</code> 的工作外包给专门的Agent，这些Agent只返回最终的 <code>Z</code> token 答案，从而保持你的主上下文清爽。</p><p>但我发现，这个强大的想法在实践中，SubAgent 会带来两个新问题：</p><ol start="1"><li><strong>它们把上下文“关起来”了 (Gatekeep Context)</strong> 如果我创建了一个 <code>PythonTests</code> SubAgent，我现在就把所有关于测试的上下文从我的<strong>主</strong> Agent那里隐藏了。主Agent再也无法对一个变更进行整体性的思考。它现在被迫调用 SubAgent 才能知道如何验证自己的代码。</li><li><strong>它们强迫 Agent 遵循人类的工作流</strong> 更糟糕的是，它们强迫 Claude 进入一个僵硬的、由人类定义的工作流。我现在是在<strong>指令它必须如何</strong> 委派任务，而这正是我希望Agent帮我解决的问题。</li></ol><p>我更喜欢的替代方案是使用 Claude 内置的 <code>Task(...)</code> 功能来生成<strong>通用</strong> 代理的克隆体。</p><p>我把所有关键上下文都放在 <code>CLAUDE.md</code> 里。然后，我让<strong>主Agent</strong> 自己决定何时以及如何将工作委派给它自己的副本。这让我既享受了SubAgent节省上下文的好处，又避免了其缺点。Agent可以动态地管理自己的协作流程。</p><p>在我的 <a href="https://blog.sshh.io/p/building-multi-agent-systems-part">《构建Multi Agent System（第二部分）》</a> 一文中，我将这种架构称为“主-克隆”模式，并且强烈推荐它，而非定制 SubAgent 所倡导的“主导-专家”模型。</p><blockquote><p><strong>核心要点：</strong></p></blockquote>
<p>自定义 SubAgent 是一个脆弱的解决方案。把上下文给你的主Agent（放在 <code>CLAUDE.md</code> 里），让它使用自己的 <code>Task/Explore(...)</code> 功能来管理任务委派。</p>
<h2 id="">恢复、继续与历史记录</h2><p>在简单的层面上，我经常使用 <code>claude --resume</code> 和 <code>claude --continue</code> 。它们对于重启一个出问题的终端或快速恢复一个旧的会话非常好用。我常常会 <code>claude --resume</code> 一个几天前的会话，只为了问Agent它是如何克服某个特定错误的，然后我用这些信息来改进我们的 <code>CLAUDE.md</code> 和内部工具。</p><p>更深入一点，<code>Claude Code</code> 将所有会话历史存储在 <code>~/.claude/projects/</code> 中，以便利用原始的历史会话数据。我有一些脚本会对这些日志进行元分析，寻找常见的异常、权限请求和错误模式，以帮助改进面向Agent的上下文。</p><blockquote><p><strong>核心要点：</strong></p></blockquote>
<p>使用 <code>claude --resume</code> 和 <code>claude --continue</code> 来重启会话和挖掘埋藏的历史上下文。</p>
<h2 id="-hooks">钩子 (Hooks)</h2><p>钩子非常重要。我个人项目不用它，但它们对于在一个复杂的企业级代码库中引导 Claude 至关重要。它们是确定性的 <strong>“必须做”</strong> 的规则，与 <code>CLAUDE.md</code> 中的 <strong>“应该做”</strong> 的建议互为补充。</p><p>我们使用两种类型的钩子：</p><ol start="1"><li><strong>提交时阻断钩子 (Block-at-Submit Hooks)</strong> 这是我们的主要策略。我们有一个 <code>PreToolUse</code> 钩子，它会包裹任何 <code>Bash(git commit)</code> 命令。它会检查一个 <code>/tmp/agent-pre-commit-pass</code> 文件，这个文件 <strong>只有</strong> 在所有测试都通过时，我们的测试脚本才会创建。如果文件不存在，钩子就会阻止提交，迫使 Claude 进入一个“测试并修复”的循环，直到构建通过。</li><li><strong>提示钩子 (Hint Hooks)</strong> 这些是简单的、非阻塞的钩子，如果Agemt正在做一些次优的操作，它们会提供“即发即忘”式的反馈。</li></ol><p>我们 <strong>刻意不使用</strong> “写入时阻断” 的钩子（例如，在 <code>Edit</code> 或 <code>Write</code> 操作上）。在Agent执行计划中途阻断它，会使它困惑甚至“沮丧”。更有效的方法是让它完成它的工作，然后在提交阶段检查最终的、完整的结果。</p><blockquote><p><strong>核心要点：</strong></p></blockquote>
<p>使用钩子在<strong>提交时</strong>强制执行状态验证（提交时阻断）。避免在<strong>写入时</strong>阻断——让 Agent 完成它的计划，然后再检查最终结果。</p><h2 id="plan">Plan模式</h2><p>对于任何使用 AI IDE 进行的“大型”功能变更，规划都是必不可少的。</p><p>对于我的个人项目，我只使用内置的Plan模式。这是一种在 Claude 开始工作前与它对齐的方式，既定义了 <strong>如何</strong> 构建某样东西，也定义了它需要停下来向我展示工作的“检查点”。经常使用这个功能可以培养一种强大的直觉，即需要提供多少最少的上下文才能得到一个好的计划，而不会让 Claude 在实现阶段搞砸。</p><p>在我们的工作代码库中，我们已经开始推广一个基于 Claude Agent SDK 构建的自定义规划工具。它与原生规划模式类似，但经过了大量提示工程，使其输出与我们现有的技术设计格式保持一致。它还能开箱即用地强制执行我们的内部最佳实践——从代码结构到数据隐私和安全。这让我们的工程师可以像一位资深架构师一样，“凭感觉规划”一个新功能（至少我们的宣传是这么说的）。</p><blockquote><p><strong>核心要点：</strong></p></blockquote>
<p>对于复杂的变更，<strong>总是</strong>使用内置的Plan模式，在Agent开始工作前先就计划达成一致。</p><h2 id="-skills">技能 (Skills)</h2><p>我同意 <a href="https://simonwillison.net/2025/Oct/16/claude-skills/">Simon Willison 的观点</a>：<strong>Skills（也许）比 MCP 更重要</strong> 。</p><p>如果你一直关注我的文章，你会知道我已经逐渐放弃在大多数开发工作流中使用 MCP，而是倾向于构建简单的 CLI（正如我在《AI 读不懂你的文档》中论述的那样）。我对Agent自主性的心智模型已经演变为三个阶段：</p><ol start="1"><li><strong>单次提示 (Single Prompt)</strong> ：在一个巨大的提示中给Agent所有上下文。（脆弱，无法扩展）。</li><li><strong>工具调用 (Tool Calling)</strong> ： “经典”的Agent模型。我们手动制作工具，为Agent抽象出现实世界。（更好，但创造了新的抽象和上下文瓶颈）。</li><li><strong>脚本化 (Scripting)</strong> ：我们给Agent访问原始环境的权限——二进制文件、脚本和文档——然后它 <strong>动态地</strong> 编写代码来与它们交互。</li></ol><p>基于这个模型，<strong>Agent Skills</strong> 显然是下一个重要功能。它们是“脚本化”这一层的正式产品化。</p><p>如果你像我一样，已经<strong>倾向于使用 CLI 而非 MCP</strong> ，那么你其实一直在不自觉地享受着 Skills 带来的好处。<code>SKILL.md</code> 文件只是一个更有组织、可共享、可发现的方式来记录这些 CLI 和脚本，并将它们暴露给智能体。</p><blockquote><p><strong>核心要点：</strong></p></blockquote>
<p>Skills 是正确的抽象。它们将基于“脚本化”的智能体模型正式化，这种模型比 MCP 所代表的僵硬的、类似 API 的模型更健壮、更灵活。</p><h2 id="-mcp"> 模型上下文协议 (MCP)</h2><p>Skills 并不意味着 MCP 已死。以前，许多人构建了糟糕的、上下文沉重的 MCP，包含几十个只是简单镜像 REST API 的工具（<code>read_thing_a()</code> , <code>read_thing_b()</code> , <code>update_thing_c()</code> ）。</p><p>“脚本化”模型（现在由 Skills 正式化）更好，但它需要一个安全的方式来访问环境。对我来说，这正是 MCP 新的、更专注的角色。</p><p>一个 MCP 不应该是一个臃肿的 API，而应该是一个简单、安全的<strong>网关</strong> ，提供几个强大的、高层次的工具：</p><ul><li><code>download_raw_data(filters…)</code></li><li><code>take_sensitive_gated_action(args…)</code></li><li><code>execute_code_in_environment_with_state(code…)</code></li></ul><p>在这个模型中，MCP 的工作不是为智能体抽象现实；它的工作是管理认证、网络和安全边界，然后让开。它为智能体提供了<strong>入口点</strong> ，然后智能体利用它的脚本能力和 markdown 上下文来完成实际工作。</p><p>我唯一还在使用的 MCP 是用于 <strong>Playwright</strong> 的，这很合理——它是一个复杂的、有状态的环境。我所有无状态的工具（如 Jira、AWS、GitHub）都已迁移到简单的 CLI。</p><blockquote><p><strong>核心要点：</strong></p></blockquote>
<p>使用扮演<strong>数据网关</strong>角色的 MCP。给智能体一两个高层次的工具（比如一个原始数据转储 API），然后让它基于这些工具编写脚本。</p><h2 id="claude-code-agent-sdk">Claude Code (Agent) SDK</h2><p><code>Claude Code</code> 不仅仅是一个交互式的 CLI；它也是一个强大的 SDK——现为 <code>Claude Agent SDK</code>，可以用来构建全新的Agent——无论用于编码还是非编码任务。对于大多数新的个人项目，我已经开始用它作为我的默认Agent框架，而不是像 LangChain/CrewAI 这样的工具。</p><p>我主要在三个方面使用它：</p><ol start="1"><li><strong>大规模并行脚本</strong> 对于大规模重构、bug 修复或迁移，我不使用交互式聊天。我编写简单的 bash 脚本，并行调用 <code>claude -p &quot;in /pathA change all refs from foo to bar&quot;</code> 。这比试图让主Agent管理几十个子Agent任务更具扩展性和可控性。</li><li><strong>构建内部聊天工具</strong> 这个 SDK 非常适合将复杂的流程包装成一个简单的聊天界面，供非技术用户使用。比如一个安装程序，在出错时可以回退到 Claude Agent SDK 来<strong>为用户修复问题</strong> 。或者一个内部的“家庭版 v0”工具，让我们的设计团队可以在我们自家的 UI 框架中“凭感觉”编写前端模型，确保他们的想法是高保真的，并且代码能更直接地用于前端生产。</li><li><strong>快速Agent原型设计</strong> 这是我最常见的用法，并且不限于编码。如果我对任何Agent任务有想法（例如，一个使用自定义 CLI 或 MCP 的“威胁调查Agent”），我会使用 Claude Code SDK 快速构建和测试原型，然后再投入到一个完整的、已部署的框架中。</li></ol><blockquote><p><strong>核心要点：</strong></p></blockquote>
<p>Claude Agent SDK 是一个强大的、通用的Agent框架。用它来批量处理代码、构建内部工具，以及在选择更复杂的框架<strong>之前</strong>快速原型化新的Agent。</p>
<h2 id="claude-code-gha-github-action">Claude Code GHA (GitHub Action)</h2><p>Claude Code GitHub Action (GHA) 可能是我最喜欢也是最被低估的功能之一。它的概念很简单：就是在 GHA 中运行 Claude Code。但正是这种简单性使其如此强大。</p><p>它与 <strong>Cursor 的 Background Agent</strong> 或 Codex 的托管 Web UI 类似，但可定制性要强得多。你控制整个容器和环境，这让你能接触到更多数据，并且至关重要的是，比任何其他产品提供更强的沙盒和审计控制。此外，它支持所有高级功能，如 Hooks 和 MCP。</p><p>我们用它来构建自定义的“随处可发 PR”的工具。用户可以从 Slack、Jira、甚至 CloudWatch 告警触发一个 PR，然后 GHA 会修复 bug 或添加功能，并返回一个经过完整测试的 PR。</p><p>由于 GHA 的日志就是完整的Agent日志，我们有一个运营流程，在公司层面定期审查这些日志，寻找常见的错误、bash 错误或不一致的工程实践。这创建了一个数据驱动的飞轮：<strong>Bugs → 改进的 <code>CLAUDE.md</code> / CLIs → 更好的Agent</strong> 。</p><pre class=""><code class="">$ query-claude-gha-logs --since 5d | claude -p “看看其他 Claude 卡在了哪里，修复它，然后提交一个PR”
</code></pre><blockquote>
<p><strong>核心要点：</strong></p></blockquote>
<p>GHA 是将 Claude Code 投入生产运营的终极方式。它将其从个人工具转变为你工程系统中一个核心的、可审计的、自我改进的部分。拥有无限自由的可能性</p><h2 id="settingsjson"> settings.json</h2><p>最后，我有一些特定的 <code>settings.json</code> 配置，我发现在个人和专业工作中都至关重要。</p><ul><li><strong>HTTPS<em>PROXY/HTTP</em>PROXY</strong> : 这对调试非常有用。我会用它来检查原始流量，看看 Claude 究竟发送了什么Prompt。对于Background Agent，它也是一个强大的细粒度网络沙盒工具。</li><li><strong>MCP<em>TOOL</em>TIMEOUT/BASH<em>MAX</em>TIMEOUT_MS</strong> : 我调高了这些值。我喜欢运行长而复杂的命令，而默认的超时时间通常过于保守。</li><li><strong>ANTHROPIC<em>API</em>KEY</strong> : 在工作中，我们使用企业 API 密钥。这将我们从“按席位付费”的许可证模式转变为“按使用量付费”的模式，这更适合我们的工作方式。</li><li><strong>“permissions”</strong> : 我会偶尔自我审计一下我允许 Claude 自动运行的命令列表。</li><li>（实际上<code>env</code>还有许多非常有用的环境变量属性）</li></ul><blockquote><p><strong>核心要点：</strong></p></blockquote>
<p>你的 <code>settings.json</code> 是进行高级定制的强大工具。</p></div><p style="text-align:right"><a href="https://dryice.icu/posts/share/claude-code#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://dryice.icu/posts/share/claude-code</link><guid isPermaLink="true">https://dryice.icu/posts/share/claude-code</guid><dc:creator><![CDATA[ice]]></dc:creator><pubDate>Thu, 26 Feb 2026 00:43:11 GMT</pubDate></item><item><title><![CDATA[Skill 与 MCP 深度对比]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://dryice.icu/posts/share/skill-mcp">https://dryice.icu/posts/share/skill-mcp</a></blockquote><div><p>首先，官方文档非常值得细细品味，看完官方文档倒也可以考虑不用看其他的文章了（针对skill，mcp这一块感觉描述一般，mcp太多东西了）：</p><p><a href="https://code.claude.com/docs/zh-CN/skills"># Agent Skills</a></p><p><a href="https://platform.claude.com/docs/zh-CN/agents-and-tools/agent-skills/overview"># Agent Skills概览</a></p><h2 id="-mcp--claude-code-">通过 MCP 将 Claude Code 连接到工具</h2><hr/><h2 id="token-">一、Token 占用对比，这个大家应该都知道，不过多描述</h2><h3 id="11-mcp">1.1 MCP：全量加载模式</h3><p>MCP 在会话启动时<strong>全量加载</strong>所有工具定义到上下文：</p><p><code>会话启动 → 加载所有 MCP 服务器 → 注入全部工具定义（JSON-Schema）</code></p><p><strong>就拿我使用的mcp来说</strong>：</p><table><thead><tr><th>MCP 服务器</th><th>工具数</th><th>Token 占用</th></tr></thead><tbody><tr><td>auggie-mcp</td><td>1</td><td>~1k</td></tr><tr><td>grok-search</td><td>5</td><td>~3k</td></tr><tr><td>memory</td><td>9</td><td>~5k</td></tr><tr><td>sequential-thinking</td><td>1</td><td>~2k</td></tr><tr><td><strong>多服务器叠加</strong></td><td>-</td><td><strong>轻松突破 10k+</strong></td></tr></tbody></table><p>随便用几个占用就非常大了</p><h3 id="12-skill">1.2 Skill：按需加载模式</h3><p>Skill 采用<strong>渐进式披露</strong>，仅在需要时加载：</p><table><thead><tr><th>阶段</th><th>加载内容</th><th>Token 占用</th></tr></thead><tbody><tr><td>启动时</td><td>仅 <code>name</code> + <code>description</code> 索引</td><td>~10-100 tokens/skill</td></tr><tr><td>触发时</td><td>完整 <code>SKILL.md</code> 内容</td><td>~1,000-5,000 tokens/skill</td></tr></tbody></table><p><strong>我使用的skill</strong>：90K 的 Skills 目录，<code>/context</code> 仅显示 <strong>513 tokens</strong>，和mcp对比起来，非常直观。</p><h3 id="13-">1.3 简单对比</h3><table><thead><tr><th>维度</th><th>MCP</th><th>Skill</th></tr></thead><tbody><tr><td>启动时占用</td><td>全量（10k+ tokens）</td><td>索引（~500 tokens）</td></tr><tr><td>使用时增量</td><td>无（已加载）</td><td>按需逐步加载</td></tr></tbody></table><hr/><h2 id="">二、设计模式对比</h2><h3 id="21-mcp--json-schema">2.1 MCP：全量注入 + JSON-Schema</h3><h4 id="-mcp">什么是 MCP？</h4><p>MCP（Model Context Protocol）即模型上下文协议，采用 <strong>Host-Client-Server</strong> 三层架构：</p><pre class=""><code class="">Host (Claude Code)
    │
    ├── Client ←──JSON-RPC──→ context7 Server              
    ├── Client ←──JSON-RPC──→ sequential-thinking Server   
    └── Client ←──JSON-RPC──→ your Server
</code></pre><pre class=""><code class=""></code></pre><table><thead><tr><th> 角色              </th><th> 是什么                                        </th></tr></thead><tbody><tr><td> <strong>Host</strong>        </td><td> AI 应用程序（比如Claude Code、VS Code、Cursor 等）    </td></tr><tr><td> <strong>Client</strong>      </td><td> 通信管道，负责与 Server 建立连接、发送 JSON-RPC 请求、接收响应   </td></tr><tr><td> <strong>Server</strong>      </td><td> 提供工具/上下文的程序（<mark class="rounded-md"><span class="px-1">日常说的「安装 MCP」就是安装 Server</span></mark>）   </td></tr><tr><td> <strong>JSON-RPC</strong>    </td><td> Client 与 Server 之间传输的数据格式（所有 MCP 都遵守此协议）   </td></tr><tr><td> <strong>Tool Schema</strong> </td><td> 启动时 Server 返回的「工具说明书」，告诉 Claude 有哪些工具、怎么调用 </td></tr></tbody></table><blockquote><p>Host 和 Client 由应用自动管理，<strong>开发者/使用者只需关注 Server</strong>。再比如 FastMCP 框架会自动处理协议细节。</p></blockquote>
<h4 id="mcp-server-python">MCP Server 目录结构（这里以Python为例）</h4><pre class=""><code class="">my-mcp-server/
├── pyproject.toml            # 包定义（必需）
│   ├── name = &quot;mcp-server-xxx&quot;
│   └── version = &quot;1.0.0&quot;
├── src/
│   ├── server.py             # 入口，初始化 FastMCP
│   ├── tools.py              # Tool 定义（单文件，适合 &lt;10 个工具）
│   ├── tools/                # Tool 定义（多文件，按功能拆分）
│   │   ├── __init__.py
│   │   ├── search.py
│   │   └── fetch.py
│   └── resources.py          # Resource 提供者（可选）
└── README.md
</code></pre><blockquote>
<p><code>tools.py</code> 和 <code>tools/</code> 二选一，可根据数量决定。</p></blockquote>
<h4 id="tool-">Tool 定义示例</h4><p>从这里就可以看出，其实和接口非常像，请求-&gt;回调数据，实际上多数的mcp也都可以理解成三方接口。</p><pre class="language-python lang-python"><code class="language-python lang-python">from mcp.server.fastmcp import FastMCP

mcp = FastMCP(&quot;weather&quot;)

@mcp.tool()
async def get_weather(location: str) -&gt; str:
    &quot;&quot;&quot;Get weather for a location.&quot;&quot;&quot;
    return f&quot;Weather in {location}: 72°F, Sunny&quot;

if __name__ == &quot;__main__&quot;:
    mcp.run(transport=&quot;stdio&quot;)
</code></pre>
<h4 id="mcpservers">客户端配置结构（mcpServers）</h4><p><strong>配置示例</strong>：</p><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;args&quot;: [
    &quot;-y&quot;,
    &quot;@upstash/context7-mcp&quot;
  ],
  &quot;command&quot;: &quot;npx&quot;,
  &quot;type&quot;: &quot;stdio&quot;
}
</code></pre><blockquote>
<p>mcp服务器的json格式，让配置起来非常方便，cv万岁</p></blockquote>
<h3 id="22-skill--markdown">2.2 Skill：渐进式披露 + Markdown</h3><h4 id="-skill">什么是 Skill？</h4><p>Skill 是 Claude Code 的<strong>知识/流程包</strong>，本质是一个 Markdown 文件夹，用于：</p><ul><li>注入领域知识（如代码规范、业务逻辑）</li><li>定义工作流程（如代码审查 SOP、部署流程）</li><li>封装可复用的脚本和模板 <mark class="rounded-md"><span class="px-1">（这直接导致 Skill 与 MCP 在使用体验上趋于相似）</span></mark></li></ul><p><strong>额外说明：</strong></p><p>Skill 只能提供指导，无法执行外部 API 调用、网络请求、会话管理等操作，毕竟只是md文件夹，但是能指导cc运行脚本，包括但不限于自带的脚本资源，这句话应该会有嚼头。</p><h4 id="skill-">Skill 目录结构（双层架构）</h4><blockquote><p>以下规范取自 <code>skill-creator</code> Skill,我觉得配置比cc官方文档里面的描述更详细</p></blockquote>
<pre class=""><code class="">skill-name/
├── SKILL.md (必需)
│   ├── YAML frontmatter ──→ 索引层（始终加载，~100 words）
│   │   ├── name: (必需)
│   │   └── description: (必需，唯一触发机制)
│   └── Markdown 正文 ────→ 内容层（触发时加载，建议 &lt;500 行）
└── Bundled Resources (可选) ──→ 内容层（按需加载）
    ├── scripts/      - 可执行脚本（Python/Bash）
    ├── references/   - 参考文档（Claude 判断需要时加载）
    └── assets/       - 输出资源（模板、图片，不加载到上下文）
</code></pre>
<p><strong>双层架构说明</strong>：</p><ul><li><strong>索引层</strong>：YAML frontmatter 中的 name/description，始终在上下文中</li><li><strong>内容层</strong>：SKILL.md 正文 + 捆绑资源，仅在触发时选择性加载</li></ul><h4 id="">核心设计原则</h4><p><strong>description 是唯一触发机制</strong></p><pre class=""><code class=""># ✅ 好的 description（包含触发场景）
description: &gt;
  Comprehensive document creation and editing with tracked changes.
  Use when Claude needs to work with .docx files for:
  (1) Creating new documents, (2) Modifying content,
  (3) Working with tracked changes, (4) Adding comments

# ❌ 差的 description（太模糊）
description: A useful document tool
</code></pre>
<h4 id="">渐进式披露模式</h4><blockquote><p>是不是觉得模式1,3非常相似？我也觉得。不过 <code>skill-creator</code> 还是分成了这三种，就全罗列出来了</p></blockquote>
<p><strong>模式 1：高层指南 + 引用</strong></p><pre class=""><code class=""># PDF Processing

## Quick start
[核心代码示例]

## Advanced features
- **Form filling**: See [FORMS.md](references/FORMS.md)
- **API reference**: See [REFERENCE.md](references/REFERENCE.md)
</code></pre><p>Claude 仅在需要时加载 FORMS.md 或 REFERENCE.md。</p><p><strong>模式 2：按领域/变体组织</strong></p><pre class=""><code class="">cloud-deploy/
├── SKILL.md (工作流 + 选择指南)
└── references/
    ├── aws.md    ← 用户选 AWS 时才加载
    ├── gcp.md
    └── azure.md
</code></pre>
<p><strong>模式 3：条件详情</strong></p><pre class=""><code class="">## Editing documents
For simple edits, modify XML directly.

**For tracked changes**: See [REDLINING.md](references/REDLINING.md)
**For OOXML details**: See [OOXML.md](references/OOXML.md)
</code></pre>
<h4 id="">工作流程</h4><pre class=""><code class="">用户请求 → Claude 扫描所有 Skill 的 description → 匹配 → 加载 SKILL.md 正文 → 按需加载 references → 执行
</code></pre>
<p><strong>特点</strong>：</p><ul><li><strong>宽松定义</strong>：Markdown 格式，自然语言描述</li><li><strong>懒加载</strong>：渐进式加载</li><li><strong>可编排</strong>：Markdown 可表达流程顺序和条件分支</li><li><strong>本质</strong>：流程/知识包（SOP 手册）</li></ul><hr/><h2 id="">三、版本管理与更新便利性</h2><p><strong>结论先行</strong>：MCP 在版本管理上<strong>绝对优于</strong> Skill。</p><h3 id="31-mcp">3.1 MCP：成熟的包管理生态</h3><h4 id="">核心机制</h4><p>MCP 直接复用 npm/PyPI 成熟生态，版本管理由包管理器自动处理。</p><p><strong>工作原理</strong>：</p><ul><li>MCP 服务器以 npm 包（js/ts）或 PyPI 包（Python）形式发布</li><li>使用 <code>npx</code> 或 <code>uvx</code> 命令启动时，包管理器会自动处理下载和缓存</li><li>版本号定义在 <code>package.json</code>（npm）或 <code>pyproject.toml</code>（PyPI）中</li></ul><p><strong>更新行为</strong>：</p><table><thead><tr><th>模式</th><th>行为</th><th>启动速度</th><th>适用场景</th></tr></thead><tbody><tr><td><strong>默认</strong></td><td>使用本地缓存，不查询 registry</td><td>快</td><td>日常使用</td></tr><tr><td><strong>强制更新</strong></td><td>每次查询 registry，版本变化时才下载（<code>@latest</code> 或 <code>--refresh</code>）</td><td>慢</td><td>需要最新版本时</td></tr></tbody></table><p><strong>获取方式对比</strong>：</p><table><thead><tr><th>获取方式</th><th>配置示例</th><th>更新机制</th></tr></thead><tbody><tr><td><strong>npx (npm)</strong></td><td><code>npx -y @pkg/server</code></td><td>默认用缓存，需 <code>@latest</code> 或清缓存强制更新</td></tr><tr><td><strong>uvx (pip)</strong></td><td><code>uvx mcp-server-fetch</code></td><td>默认用缓存，需 <code>--refresh</code> 或清缓存强制更新</td></tr><tr><td><strong>本地命令</strong></td><td><code>auggie --mcp</code></td><td>需手动 <code>pip install --upgrade</code></td></tr></tbody></table><p><strong>配置示例</strong>：</p><pre class="language-json lang-json"><code class="language-json lang-json">// ~/.claude.json
{
  &quot;mcpServers&quot;: {
    &quot;memory&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;@modelcontextprotocol/server-memory&quot;]
    }
  }
}
</code></pre>
<h4 id="">如何更新</h4><p><strong>推荐方式：手动清理缓存</strong></p><p>当 MCP 服务器有新版本时，清理缓存即可：</p><pre class=""><code class=""># 清理 npx 缓存
rm -rf ~/.npm/_npx #（不推荐！不推荐！不推荐！）

# 清理 uvx 缓存
uv cache clean #（不推荐！不推荐！不推荐！）

# 上述的清理会清理系统中所有的缓存，但mcp的缓存位置各有不同，需要主动寻找，且无规律可言，最好还是让cc自己来吧
</code></pre>
<p><strong>工作流程</strong>：</p><ol start="1"><li>清理缓存</li><li>重启 Claude Code</li><li>自动下载最新版本</li></ol><p><strong>优点</strong>：日常启动快，需要更新时才清理缓存</p><p><strong>不推荐使用 @latest 或 --refresh</strong></p><p>虽然可以自动更新，但会显著影响启动速度（每次都查询 registry）。</p><p>配置示例：</p><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;mcpServers&quot;: {
    &quot;memory&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;@modelcontextprotocol/server-memory@latest&quot;]  // npx 使用 @latest
    },
    &quot;fetch&quot;: {
      &quot;command&quot;: &quot;uvx&quot;,
      &quot;args&quot;: [&quot;--refresh&quot;, &quot;mcp-server-fetch&quot;]  // uvx 使用 --refresh
    }
  }
}
</code></pre>
<hr/><h3 id="32-skill">3.2 Skill：普遍缺乏版本管理</h3><h4 id="-skill-">现状：一般来说 Skill 没有版本管理</h4><p>与 MCP 不同，<strong>一般来说 Skill 没有任何版本管理机制</strong>：</p><ul><li>没有版本号定义</li><li>没有版本锁定</li><li>手动管理</li></ul><p>Skill在某些时候更像可以随意定制的小玩具，在原本的基础上又能随意修改</p><p><strong>Skill 的获取与更新方式</strong>：</p><table><thead><tr><th>获取方式</th><th>更新方式</th><th>版本控制</th></tr></thead><tbody><tr><td>GitHub 克隆</td><td><code>git pull</code> 手动更新</td><td>依赖 Git commit hash</td></tr><tr><td>下载ZIP文件</td><td>手动替换文件</td><td><strong>无</strong></td></tr><tr><td>CC直接生成</td><td>即时生效</td><td><strong>无</strong></td></tr></tbody></table><p><strong>问题</strong>：</p><ul><li>用户不知道 Skill 是否有新版本</li><li>无法回滚到特定版本（除非使用 Git）</li></ul><hr/><h4 id="-marketplace">例外：官方 Marketplace（需登录）</h4><p><strong>唯一的版本管理方案</strong>是官方 Marketplace，但有严格限制：</p><ul><li><strong>必须登录</strong>：API Key 用户无法使用</li><li><strong>非标准化</strong>：没有强制的版本号格式</li></ul><p><strong>Marketplace 版本管理示例</strong>：</p><pre class="language-json lang-json"><code class="language-json lang-json">// marketplace.json
{
  &quot;name&quot;: &quot;my-plugins&quot;,
  &quot;plugins&quot;: [
    {
      &quot;name&quot;: &quot;review-plugin&quot;,
      &quot;source&quot;: &quot;./plugins/review-plugin&quot;,
      &quot;version&quot;: &quot;2.1.0&quot;  // ← 需要手动维护
    }
  ]
}
</code></pre>
<p><strong>限制</strong>：</p><ul><li>仅适用于官方 Marketplace 中的 Skill</li><li>无法用于自定义或第三方 Skill</li><li>API Key 用户完全无法使用</li></ul><p>ps：这一块应该是这样的吧，没有官号诶</p><hr/><h4 id="httpslinuxdottopic1485159p-12803168-skill-25skill-"><a href="https://linux.do/t/topic/1485159#p-12803168-skill-25"></a>Skill 的独特优势</h4><p>尽管缺乏版本管理，Skill 还有一个 MCP 无法比拟的优势：</p><p><strong>即时生效</strong>：修改 Skill 文件后无需重启服务，下次触发时自动加载最新内容。</p><p><strong>对比 MCP</strong>：</p><ul><li><strong>MCP</strong>：修改后需重启 Claude Code 才能生效</li><li><strong>Skill</strong>：适合快速迭代和调试，改完即用</li></ul><p><strong>适用场景</strong>：</p><ul><li>个人定制化 Skill（频繁调整）</li><li>快速原型验证</li></ul><hr/><h2 id="-slash-command-">四、我推荐：使用 Slash Command 稳定触发</h2><p><strong>事先声明：</strong> 这里的/ccg不是使用的skill，我才发现，哎呀！！</p><p>但是我都按照这个写了，不想再改了。</p><p><code>/ccg</code>的工作流程是 /cmd → 自己的脚本<br/>请你们假装 是 /cmd → skill → skill的脚本</p><h3 id="41-skill-">4.1 Skill 的触发不确定性问题</h3><p>Skill 依赖 Claude <strong>自动匹配描述</strong>，存在以下问题：</p><ul><li>该触发时未触发（描述不够精准）</li><li>不该触发时误触发（描述过于宽泛）</li></ul><p><strong>实际体验</strong>：虽然 Skill 理论上能够自动触发，但实际触发率较低。比如我即使在 CLAUDE.md 中明确写了&quot;让 Codex 和 Gemini 参与协作&quot;，Claude 偶尔还是会忽略。所以之前我都会主动在需求后面添加这句话。</p><p><strong>解决方案</strong>：使用 cmd 可以 <strong>100% 稳定触发</strong>。例如：<code>/ccg:feat 需求描述</code> 总比主动描述或者期待cc记得安心。</p><h3 id="42-skill--command-">4.2 最佳实践：Skill + Command 工作流组合</h3><p><strong>为什么需要 Command</strong>：</p><p>Skill 可能包含复杂的命令调用和参数配置，这些细节难以通过自然语言稳定触发。例如：</p><ul><li>外部工具调用：<code>/home/nobug/.claude/bin/codeagent-wrapper --backend gemini</code></li><li>带参数的指令：<code>--backend codex</code>、<code>--SESSION_ID xxx</code></li><li>多阶段工作流：需要按特定顺序执行多个步骤</li></ul><p>这些精确的指令和参数通过语言描述很难稳定触发，而 Command 可以将这些细节明确定义在文件中。</p><p><strong>模式 1 示例</strong>（简单流程）：</p><pre class=""><code class="">场景：应用主题到 Artifact

├── ~/.claude/skills/theme-factory/
│   └── SKILL.md                    # 主题定义、应用指南
└── ~/.claude/commands/
    └── theme-factory.md            # 内容：&quot;Execute the theme-factory skill&quot;

用户输入 /theme-factory 应用深色主题
    ↓
Command 触发 → Claude 加载 SKILL.md → 自主执行
</code></pre>
<p><strong>模式 2 示例</strong>（复杂流程）：</p><pre class=""><code class="">场景：前端专项开发

└── ~/.claude/commands/ccg/
    └── frontend.md                 # 完整工作流定义

用户输入 /ccg:frontend 实现响应式导航栏
    ↓
Claude 加载 frontend.md 内容
    ↓
按照 Command 中定义的 6 个阶段执行：
  1. Prompt 增强（可选）
  2. 研究（代码检索）
  3. 构思（调用 Gemini 分析）
  4. 计划（调用 Gemini 规划）
  5. 执行（Claude 实施）
  6. 优化（调用 Gemini 审查）
</code></pre>
<p><strong>外部工具说明</strong>：像 <code>codeagent-wrapper</code> 这样的工具是<strong>外部可执行文件</strong>（位于 <code>~/.claude/bin/</code>），不是 Skill。Skill 只能提供指导，无法执行外部 API 调用、网络请求、会话管理等操作，这些必须由外部工具完成。</p><p><strong>核心原则</strong>：</p><ul><li><strong>稳定性优先</strong>：复杂流程用 Command 定义，确保稳定执行</li><li><strong>外部工具必须用 Command</strong>：Skill 无法执行外部命令</li><li><strong>简单场景用 Skill</strong>：减少维护成本，提高灵活性</li></ul></div><p style="text-align:right"><a href="https://dryice.icu/posts/share/skill-mcp#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://dryice.icu/posts/share/skill-mcp</link><guid isPermaLink="true">https://dryice.icu/posts/share/skill-mcp</guid><dc:creator><![CDATA[ice]]></dc:creator><pubDate>Thu, 05 Feb 2026 00:31:55 GMT</pubDate></item><item><title><![CDATA[解决Appimage图标问题]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/15/20260115163357891_repeat_1768466039207__085692.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/15/20260115164449613_repeat_1768466691924__031496.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/15/20260115164411094_repeat_1768466652455__330716.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/15/20260115164217172_repeat_1768466544495__326041.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/15/20260115165438368_repeat_1768467279758__809843.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/15/20260115165840593_repeat_1768467521770__296207.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/15/20260115165535778_repeat_1768467336897__491568.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/15/20260115165923589_repeat_1768467564682__188981.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://dryice.icu/posts/share/appimage-gnome">https://dryice.icu/posts/share/appimage-gnome</a></blockquote><div><p>GNOME桌面使用appimagelauncherd 安装Appimage 程序底部栏有的图标会显示为默认图标，这对于有强迫症的人来说这是大问题。</p><p><img src="https://img.dryice.icu/images/2026/01/15/20260115163357891_repeat_1768466039207__085692.png" alt="image.png" height="784" width="1756"/></p>
<p>我遇到的两个软件就有这个问题。一个是 PicGo,一个是 Navicat。如上图所示。运行navicat底下显示的是GNOME默认给的一个默认图标。我很奇怪我的Obsidian也是Appimage运行的为啥就这两货运行起来图标就不能正常显示呢？</p><p>查询了一写资料，也有人遇到类似情况，好像换一个Appimage管理工具就好了不用appimagelauncherd。没有别的办法吗？问了一些啊伟大的 gemini。试了一下真解决了。</p><p>使用下面这个命令，然后点击 Navicat 窗口。（或者直接鼠标悬浮在运行程序上面就会显示 AppRun,从上图可以看到。）</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">xprop WM_CLASS
</code></pre>
<p>会返回下面内容，这样对对应为上面图中鼠标悬浮在上面显示 AppRun。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">WM_CLASS(STRING) = &quot;AppRun&quot;, &quot;AppRun&quot;
</code></pre>
<p>然后去<code>/home/ice/.local/share/applications/</code> 下面找到启动程序快捷方式。往里面添加一个下面参数。AppRun 也对应我们上面抓取到的。</p><pre class=""><code class="">StartupWMClass=AppRun
</code></pre>
<p><img src="https://img.dryice.icu/images/2026/01/15/20260115164449613_repeat_1768466691924__031496.png" alt="image.png"/></p><p><img src="https://img.dryice.icu/images/2026/01/15/20260115164411094_repeat_1768466652455__330716.png" alt="image.png"/></p><p>也可以使用Pins去修改。(可以通过 <code>flatp install io.github.fabrialberio.pinapp</code> 安装Pins)
<img src="https://img.dryice.icu/images/2026/01/15/20260115164217172_repeat_1768466544495__326041.png" alt="image.png"/></p>
<p>修改好之后就正常了</p><p><img src="https://img.dryice.icu/images/2026/01/15/20260115165438368_repeat_1768467279758__809843.png" alt="image.png"/></p>
<p>然后我就去看了一下PicGo 里的参数。发现是 PicGo。鼠标悬浮在运行的程序上面发现名称是 picgo。改成picgo。果然就好了。</p>
<p><img src="https://img.dryice.icu/images/2026/01/15/20260115165840593_repeat_1768467521770__296207.png" alt="image.png"/></p><p><img src="https://img.dryice.icu/images/2026/01/15/20260115165535778_repeat_1768467336897__491568.png" alt="image.png"/></p><p><img src="https://img.dryice.icu/images/2026/01/15/20260115165923589_repeat_1768467564682__188981.png" alt="image.png"/></p></div><p style="text-align:right"><a href="https://dryice.icu/posts/share/appimage-gnome#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://dryice.icu/posts/share/appimage-gnome</link><guid isPermaLink="true">https://dryice.icu/posts/share/appimage-gnome</guid><dc:creator><![CDATA[ice]]></dc:creator><pubDate>Thu, 15 Jan 2026 09:18:16 GMT</pubDate></item><item><title><![CDATA[Fedora (GNOME) 美化指南]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/13/20260113153412364_repeat_1768289653462__066703.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/13/20260113153459732_repeat_1768289700852__089276.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/13/20260113153613073_repeat_1768289774470__954215.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/13/20260113153641035_repeat_1768289803030__114164.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/13/20260113153701043_repeat_1768289822163__687775.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/13/20260113153724536_repeat_1768289845628__055279.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/13/20260113153739363_repeat_1768289860657__734032.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/13/20260113153754683_repeat_1768289875733__690477.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/13/20260113153816433_repeat_1768289898493__843021.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/13/20260113153828899_repeat_1768289909994__096517.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/13/20260113153845509_repeat_1768289927577__654849.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2026/01/13/20260113153910451_repeat_1768289951567__838959.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://dryice.icu/posts/share/gnomebeautifui">https://dryice.icu/posts/share/gnomebeautifui</a></blockquote><div><p>魔法工具安装，懂的都懂</p><h2 id="">输入法</h2><p>首先当然要安装一下输入法啦
原生代理ibus联想不是很好，这里我们选择安装Fcitx5输入法</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">sudo dnf install fcitx5 fcitx5-chinese-addons fcitx5-configtool
</code></pre><p>添加自启动 安装自启动脚本，让Fcitx5开机运行</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">sudo dnf install fcitx5-autostart
</code></pre><p>配置环境变量 <code>/etc/environment</code></p><pre class=""><code class="">GTK_IM_MODULE=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx
</code></pre>
<h2 id="">字体补充</h2><p>网页 <code>leetcode</code> 需要字体 <code>Droid Sans Mono</code> (刷题党必备需要哈哈哈)</p>
<pre class="language-bash lang-bash"><code class="language-bash lang-bash">sudo dnf install google-droid-sans-mono-fonts
</code></pre>
<p>idea GUI 界面中文字体显示错误，显示为方块</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># 安装 思源黑体 (Adobe Source Han Sans) 和 文泉驿微米黑
sudo dnf install adobe-source-han-sans-cn-fonts wqy-microhei-fonts wqy-zenhei-fonts

# 安装中文语言包支持（这会补齐大部分缺失的依赖）
sudo dnf install langpacks-zh_CN
</code></pre>
<p>解决“可变字体”兼容问题 (可选)</p><p>Fedora 默认安装的是 <code>google-noto-sans-cjk-vf-fonts</code>（VF 代表 Variable Font）。某些版本的 IntelliJ IDEA (Java) 无法正确解析这种字体，导致显示方块。</p><p>替换为静态版本：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># 移除可变字体版本
sudo dnf remove google-noto-sans-cjk-vf-fonts

# 安装静态版本
sudo dnf install google-noto-sans-cjk-fonts google-noto-serif-cjk-fonts

# 手动刷新字体缓存
sudo fc-cache -fv
</code></pre>
<p>jetbrains 系列字体</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">sudo dnf install jetbrains-mono-fonts-all
</code></pre><p>jetbrains 字体的一些补充（有的需要重启才生效）
<a href="https://download.jetbrains.com/fonts/JetBrainsMono-2.304.zip?_cl=MTsxOzE7RGpqTGhJZzhqTENXWm9namliZlh4dUV5cVM5dlRoaldJMVBMOUFXZWRZcFNBVFFVamFCamdjckdGcW9oSzNZNzs=&amp;_gl=1*k8r7iq*_gcl_au*ODk4MTMzNjI0LjE3Njc5NTIzNDU.*FPAU*ODk4MTMzNjI0LjE3Njc5NTIzNDU.*_ga*MTM2NDE3MzE2Mi4xNzY3OTUyMzQ1*_ga_9J976DJZ68*czE3NjgxMDY5NzAkbzMkZzAkdDE3NjgxMDY5NzQkajU2JGwwJGgw">官网下载地址</a>  --- <a href="https://release-assets.githubusercontent.com/github-production-release-asset/27574418/c2cb9596-7c30-4241-8421-a0e6d8ab1112?sp=r&amp;sv=2018-11-09&amp;sr=b&amp;spr=https&amp;se=2026-01-11T04%3A40%3A46Z&amp;rscd=attachment%3B+filename%3DJetBrainsMono.zip&amp;rsct=application%2Foctet-stream&amp;skoid=96c2d410-5711-43a1-aedd-ab1947aa7ab0&amp;sktid=398a6654-997b-47e9-b12b-9515b896b4de&amp;skt=2026-01-11T03%3A40%3A18Z&amp;ske=2026-01-11T04%3A40%3A46Z&amp;sks=b&amp;skv=2018-11-09&amp;sig=cFw%2B6IKeDkWPDB0MXHqiWQjq9tiXAFAvBxti1ELb6Iw%3D&amp;jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmVsZWFzZS1hc3NldHMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwia2V5Ijoia2V5MSIsImV4cCI6MTc2ODEwNzMzNSwibmJmIjoxNzY4MTAzNzM1LCJwYXRoIjoicmVsZWFzZWFzc2V0cHJvZHVjdGlvbi5ibG9iLmNvcmUud2luZG93cy5uZXQifQ.NmA6TIvvqdvjHTNSJh1-e7041KtKC2sfZCbdVLW96AA&amp;response-content-disposition=attachment%3B%20filename%3DJetBrainsMono.zip&amp;response-content-type=application%2Foctet-stream">其他地址</a></p><p>下载解压把 <code>.ttf</code> 文件放入到 <code>/usr/share/fonts</code> 目录下面</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">unzip JetBrainsMono-2.304.zip -d jetbrains-font
</code></pre>
<p><strong>创建字体目录：</strong> 你可以选择安装给“当前用户”或“全系统”：
仅当前用户：<code>mkdir -p ~/.local/share/fonts</code>
全系统（推荐）：<code>sudo mkdir -p /usr/share/fonts/jetbrains</code>
移动字体文件：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># 假设你安装给全系统
sudo cp -r jetbrains-font/fonts/ttf/*.ttf /usr/share/fonts/jetbrains/
</code></pre>
<p>这里的字体可能还不是很全如wps中的一些字体，但是应该够了</p>
<h2 id="">终端美化</h2><p>这里终端模拟器选用的是<code>ghostty</code>，<code>sh</code>工具用的是<code>zsh</code>
为啥用 <code>ghostty</code>，请看 其他的像 <code>gnome-terminal</code>  <code>Ptyxis</code> 都对 <code>starship</code> 不是很好 只有<code>ghostty</code>左右的圆角处理的比较好，这里没有去试<code>oh-my-zsh</code> 听说也很不错，下次出一个 <code>oh-my-zsh</code>配置。</p><p><img src="https://img.dryice.icu/images/2026/01/13/20260113153412364_repeat_1768289653462__066703.png" alt="img" height="441" width="690"/></p><ol start="1"><li>安装<code>zsh</code> 和 <code>ghostty</code></li></ol><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># 安装 ghostty
sudo dnf install --nogpgcheck --repofrompath &#x27;terra,https://repos.fyralabs.com/terra$releasever&#x27; terra-release
sudo dnf install ghostty

# 安装 zsh
sudo dnf install zsh

# 语法高亮和自动补全
sudo dnf install zsh-syntax-highlighting zsh-autosuggestions

# fedora 43 不支持
# 1. 启用第三方仓库
# sudo dnf copr enable @zsh-users/zsh-completions
# 2. 安装插件
# sudo dnf install zsh-completions
</code></pre>
<ol start="2"><li>下载starship</li></ol><pre class=""><code class=""># 添加源
sudo dnf copr enable atim/starship
sudo dnf install starship

# 或者脚本安装
curl -sS https://starship.rs/install.sh | sh
</code></pre>
<p><a href="https://starship.rs/presets/gruvbox-rainbow">主题挑选</a>
我选的是<code>gruvbox-rainbow </code>主题
<a href="https://starship.rs/presets/toml/gruvbox-rainbow.toml">主题下载gruvbox-rainbow</a></p><p>下载好的<code>toml</code>文件重命名为<code>starship.toml</code> 放到 <code>~/.config/</code> 目录下</p><p>编辑zshrc，加入如下内容</p><pre class=""><code class="">eval &quot;$(starship init zsh)&quot;
</code></pre>
<p>附上我完整的zsh配置，里面包括了语法高亮和补全。类似下面的效果，只要按以下方向键<code>-&gt;</code>，就能补全。还是很方便的</p><p><img src="https://img.dryice.icu/images/2026/01/13/20260113153459732_repeat_1768289700852__089276.png" alt="img"/></p><pre class=""><code class=""># Created by newuser for 5.9
#

eval &quot;$(starship init zsh)&quot;

# arch linux
#语法检查和高亮
#source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
#source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh

# fedora
# 语法检查和高亮
source /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
source /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh

#开启tab上下左右选择补全
zstyle &#x27;:completion:*&#x27; menu select
autoload -Uz compinit
compinit

# 设置历史记录文件的路径
HISTFILE=~/.zsh_history

# 设置在会话（内存）中和历史文件中保存的条数，建议设置得大一些
HISTSIZE=1000
SAVEHIST=1000

# 忽略重复的命令，连续输入多次的相同命令只记一次
setopt HIST_IGNORE_DUPS

# 忽略以空格开头的命令（用于临时执行一些你不想保存的敏感命令）
#setopt HIST_IGNORE_SPACE

# 在多个终端之间实时共享历史记录
# 这是实现多终端同步最关键的选项
setopt SHARE_HISTORY

# 让新的历史记录追加到文件，而不是覆盖
setopt APPEND_HISTORY
# 在历史记录中记录命令的执行开始时间和持续时间
setopt EXTENDED_HISTORY

export NVM_DIR=&quot;$HOME/.nvm&quot;
[ -s &quot;$NVM_DIR/nvm.sh&quot; ] &amp;&amp; \. &quot;$NVM_DIR/nvm.sh&quot;  # This loads nvm              [ -s &quot;$NVM_DIR/bash_completion&quot; ] &amp;&amp; \. &quot;$NVM_DIR/bash_completion&quot;  # This loads nvm bash_completion
                                                                                alias clash=&#x27;nohup env WEBKIT_DISABLE_COMPOSITING_MODE=1 clash-verge &gt; ~/clash_verge.log 2&gt;&amp;1 &amp;&#x27;
                                                                   alias jep=&#x27;nohup env WEBKIT_DISABLE_COMPOSITING_MODE=1 /home/ice/file/zip/jetbrains-crack-toolbox_2.2.0_linux/jetbrains-crack-toolbox &gt; ~/jetbrains-crack.log 2&gt;&amp;1 &amp;&#x27;

# 开启代理
proxy() {
  export http_proxy=&quot;http://127.0.0.1:7897&quot;
  export https_proxy=&quot;http://127.0.0.1:7897&quot;
  export HTTP_PROXY=&quot;$http_proxy&quot;
  export HTTPS_PROXY=&quot;$https_proxy&quot;
  export NO_PROXY=&quot;localhost,127.0.0.1,::1&quot;
  export no_proxy=&quot;$NO_PROXY&quot;
}

# 取消代理
unproxy() {
  unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY no_proxy NO_PROXY
}

alias switch-ohmyzsh=&#x27;source ~/.zshrc.zsh-init&#x27;

alias cls=&#x27;clear&#x27;
alias ll=&#x27;ls -l&#x27;

# Docker 容器简化格式化输出
alias dpsf=&#x27;docker ps --format &quot;table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Ports}}&quot;&#x27;
alias dps=&#x27;docker ps&#x27;
alias dpsa=&#x27;docker ps -a&#x27;
alias dpsm=&#x27;docker ps --format &quot;table {{.ID}}\t{{.Image}}\t{{.Names}}&quot;&#x27;
</code></pre>
<p>重启终端就能看见配置好了，效果如下</p><p><img src="https://img.dryice.icu/images/2026/01/13/20260113153613073_repeat_1768289774470__954215.png" alt="img"/></p><ol start="3"><li>终端模拟器美化
下载 <code>ghostty</code> 主题 <a href="https://github.com/catppuccin/ghostty/blob/main/themes/catppuccin-frappe.conf">下载地址</a></li></ol><p>将下载好的文件放到一个位置 ，为这里放到了 <code>/home/ice/.config/ghostty/</code>
编辑 <code>ghostty</code> 的配置文件。位置在 <code>/home/ice/.config/ghostty/config</code>
加入如下内容</p><pre class="language-text lang-text"><code class="language-text lang-text">theme = /home/ice/.config/ghostty/catppuccin-frappe.conf

# 透明程度
background-opacity = 0.85

# 字体
font-family = &quot;Adwaita Mono&quot;

# 字体大小
font-size = 15

#隐藏标题栏
window-decoration = none

#设置左右边距
window-padding-x=10
#设置上下边距
window-padding-y=10

# 初始化高度和宽度
window-height = 28
window-width = 95
</code></pre><h2 id="">桌面美化</h2><p>显卡驱动安装</p>
<pre class="language-bash lang-bash"><code class="language-bash lang-bash">sudo dnf install \
https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \
https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm
sudo dnf makecache
sudo dnf install akmod-nvidia xorg-x11-drv-nvidia-cuda
</code></pre>
<p>ExtensionManager拓展下载(这里用的是flatpak 下载会沙箱化，不推荐用这个安装微信，好像一些位置复制的文件不能粘贴到对话框，粘过去的只有路径)，这里重新添加源之后应该就能处理fedora的gnome-software 一直刷新的问题。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">sudo dnf install flatpak
# 解决源不生效的问题
flatpak remote-delete fedora
flatpak remote-delete flathub
# 重新添加
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
# 安装拓展管理器
flatpak search ExtensionManager
flatpak install flathub com.mattjakeman.ExtensionManager

</code></pre>
<p>拓展推荐
<img src="https://img.dryice.icu/images/2026/01/13/20260113153641035_repeat_1768289803030__114164.png" alt="img"/></p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">Blur my Shell  # 他会让你桌面上面以及周围 是亚克力效果。很不错
Burn my Shell  # 窗口打开、关闭 特效
Caffeine  # 他会让你的屏幕保持常亮
Input Method Panel # 输入法美化
Logo Menu # 左上角的图标 支持自定义
Vitals # 他会显示网速等信息
Lock screen background #锁屏壁纸
AppIndicator and KStatusNotifierItem Support  # 他会在状态栏显示应用
Clipboard Indicator # 剪切板历史
Lock Keys  # 大写锁数字键开启情况
</code></pre>
<p>对应拓展的效果
Blur my Shell 开启和没有开启
<img src="https://img.dryice.icu/images/2026/01/13/20260113153701043_repeat_1768289822163__687775.png" alt="img"/></p><p>Input Method Panel 开启和为开启效果
<img src="https://img.dryice.icu/images/2026/01/13/20260113153724536_repeat_1768289845628__055279.png" alt="img"/></p><p>Logo Menu 苹果标就是他啦</p><p><img src="https://img.dryice.icu/images/2026/01/13/20260113153739363_repeat_1768289860657__734032.png" alt="img"/></p><p>Vitals 他会显示网速等信息</p>
<p>AppIndicator and KStatusNotifierItem Support 他会在状态栏显示应用
<img src="https://img.dryice.icu/images/2026/01/13/20260113153754683_repeat_1768289875733__690477.png" alt="img"/></p><p>Clipboard Indicator  剪切板历史，可自定义快捷键
<img src="https://img.dryice.icu/images/2026/01/13/20260113153816433_repeat_1768289898493__843021.png" alt="img"/></p>
<p>Lock Keys 大写锁数字键开启情况
<img src="https://img.dryice.icu/images/2026/01/13/20260113153828899_repeat_1768289909994__096517.png" alt="img"/></p>
<p>下载Pins</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># Pins 可以隐藏快捷方式以及更换快捷方式图标,以及添加启动参数，比如我的clash-verge 启动闪退在独显模式，需要加 `WEBKIT_DISABLE_COMPOSITING_MODE=1` 这个参数才行
flatpak install io.github.fabrialberio.pinapp
</code></pre>
<p><img src="https://img.dryice.icu/images/2026/01/13/20260113153845509_repeat_1768289927577__654849.png" alt="img"/></p>
<p><img src="https://img.dryice.icu/images/2026/01/13/20260113153910451_repeat_1768289951567__838959.png" alt="img"/></p><h2 id="grub">GRUB启动美化</h2><p>你可以去 <a href="https://www.gnome-look.org/browse?cat=109">Gnome-look.org</a> 寻找喜欢的 GRUB 主题。这里我用的是：Vinceliuice 的 GRUB 主题库，有四款主题可选 <code>tela|vimix|stylish|whitesur</code>。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">git clone https://github.com/vinceliuice/grub2-themes.git
cd grub2-themes
# sudo ./install.sh -t whitesur -s 2k
# sudo ./install.sh -t whitesur -c 2560x1600
sudo ./install.sh -t whitesur -c 1920x1080
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
</code></pre>
<pre class="language-text lang-text"><code class="language-text lang-text">  -t, --theme                 theme variant(s)          [tela|vimix|stylish|whitesur]       (default is tela)
  -i, --icon                  icon variant(s)           [color|white|whitesur]              (default is color)
  -s, --screen                screen display variant(s) [1080p|2k|4k|ultrawide|ultrawide2k] (default is 1080p)
  -c, --custom-resolution     set custom resolution     (e.g., 1600x900)                    (disabled in default)
  -r, --remove                remove theme              [tela|vimix|stylish|whitesur]       (must add theme name option, default is tela)

  -b, --boot                  install theme into &#x27;/boot/grub&#x27; or &#x27;/boot/grub2&#x27;
  -g, --generate              do not install but generate theme into chosen directory       (must add your directory)

  -h, --help                  show this help
</code></pre>
<p>暂时就这么多了，后续有的话再补充，这个应该适用于所有gnome桌面。</p></div><p style="text-align:right"><a href="https://dryice.icu/posts/share/gnomebeautifui#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://dryice.icu/posts/share/gnomebeautifui</link><guid isPermaLink="true">https://dryice.icu/posts/share/gnomebeautifui</guid><dc:creator><![CDATA[ice]]></dc:creator><pubDate>Tue, 13 Jan 2026 07:51:10 GMT</pubDate></item><item><title><![CDATA[记录 2026 年第 6 天]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://dryice.icu/notes/5">https://dryice.icu/notes/5</a></blockquote><p>高中一走就是一个月，大学一走就是半年，打工一走就是一年，人生一走就是一辈子，到底什么样的终点才配得上这一路的颠沛流离。
小时候忘记带作业是天大的事，高中时觉得考不上大学是天大的事，恋爱的时候觉得跟喜欢的人分开是天大的事，但现在回头看看那些难以跨过的山其实都跨过了，以为不能接受的也都接受了，生活里充满了想象，遗憾也不过是常态。
好在许多风景路过我们身旁，我们也都一个人看过许多傍晚的夕阳，其实人就是无论做什么选择都会后悔，大家总是习惯性地去美化当时没有选择的那条路，可是大家都心知肚明，就算时间重来一次，以当时的心智和阅历大概率还是会做出同样的选择，那么故事的结局还重要吗？
人生几十年是享受过程的一场修行，失之东隅，收之桑榆回头看，轻舟已过万重山，向前看，前路漫漫亦灿灿</p><p style="text-align:right"><a href="https://dryice.icu/notes/5#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://dryice.icu/notes/5</link><guid isPermaLink="true">https://dryice.icu/notes/5</guid><dc:creator><![CDATA[ice]]></dc:creator><pubDate>Tue, 06 Jan 2026 14:45:04 GMT</pubDate></item><item><title><![CDATA[frp 入门案例]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://dryice.icu/posts/technology/frp">https://dryice.icu/posts/technology/frp</a></blockquote><div><p>经典的内网穿透方案</p><p>什么是内网穿透？举个栗子</p><p>假设你在家里，突然想连公司内网的一台 MySQL 数据库——但它压根没暴露在公网上，防火墙一关，外人连端口都扫不到。</p><p>怎么办？<br/>你可以让公司那台机器主动“打个洞”，把本地的 3306 端口（MySQL 默认端口）映射到一台你掌控的公网服务器上。比如：<br/>👉 公司机器 → 主动连上你的云服务器 → 把自己的 3306 暴露成云服务器的 6306。</p><p>然后你在家里，连 <code>your-server-ip:6306</code>，就等于连上了公司内网的数据库。</p><p>听起来很方便？<br/><strong>但——这真的很危险！</strong><br/>等于你亲手给内网开了一扇没上锁的后门。要是没做身份验证、流量加密、访问控制，黑客顺着这个洞摸进来，轻则数据泄露，重则整台机器沦陷。</p><h3 id="">那为啥还有人用内网穿透？</h3><p>道理很简单：<strong>穷 + 爱折腾 😅</strong>
比如：</p><ul><li>你有台 4 核 8G 的老笔记本，想跑个 Spring Boot + PostgreSQL 项目；</li><li>但你没有公网 IP（家里宽带是 NAT 后的，连路由器管理页都进不去）；</li><li>云服务器又贵——买台轻量应用服务器还要几十块一个月。
这时候，内网穿透就成了“低成本上云”的曲线救国方案：</li></ul><p>实操
这里需要被准备一台有公网ip的服务器，也有不需要公网ip的方案，我之前写过通过cloudeflare 和一个域名就能实现内网穿透，当然这有点慢。</p><p>下面是需要公网地址的代理方案。</p><h2 id="frp-">frp 服务端</h2><p>在frp服务端启动服务，做好tocken校验，防止谁都能用你的frp server</p><p><a href="https://github.com/fatedier/frp/releases">frp下载地址</a></p><p>下载好后解压</p><p>在服务端你可以这么写</p><pre class="language-toml lang-toml"><code class="language-toml lang-toml"># 监听地址 &amp; 控制端口（客户端靠这个连上来）
bindAddr = &quot;0.0.0.0&quot;
bindPort = 7000

# 🔐 强烈建议加 token！防“搭便车”
auth.method = &quot;token&quot;
auth.token = &quot;你生成的随机长字符串，比如 openssl rand -hex 16&quot;

# 可选：开个 dashboard，方便看状态
webServer.addr = &quot;0.0.0.0&quot;
webServer.port = 7500
webServer.user = &quot;admin&quot;
webServer.password = &quot;别用 123456…… 换个复杂点的&quot;

# （进阶）想更安全？开 TLS（需要证书）
# transport.tls.force = true
# transport.tls.certFile = &quot;/path/to/server.crt&quot;
# transport.tls.keyFile = &quot;/path/to/server.key&quot;
</code></pre>
<p>启动frp 服务端</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">./frps -c frps.toml
# 建议用 systemd 或 supervisor 守护，别直接前台跑
</code></pre>
<h2 id="frp-">frp 客户端</h2><p>可以有多个客户端</p><pre class="language-toml lang-toml"><code class="language-toml lang-toml">erverAddr = &quot;你云服务器的公网 IP&quot;
serverPort = 7000

# 必须和服务端一致！否则连不上
auth.method = &quot;token&quot;
auth.token = &quot;和上面一样的 token&quot;

# 示例1：穿透 SSH（方便远程连内网机器）
[[proxies]]
name = &quot;ssh-to-dev&quot;
type = &quot;tcp&quot;
localIP = &quot;127.0.0.1&quot;   # 或具体内网 IP，如 192.168.1.100
localPort = 22
remotePort = 1022       # 公网访问时用 your-ip:1022 连 SSH

# 示例2：穿透 MySQL（⚠️ 谨慎开放！）
[[proxies]]
name = &quot;mysql-local&quot;
type = &quot;tcp&quot;
localIP = &quot;127.0.0.1&quot;
localPort = 3308        # 你本机 MySQL 实际端口
remotePort = 13309      # 公网访问端口 —— 别用 3306！避开扫描器默认目标
</code></pre><p>启动客户端：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">./frpc -c frpc.toml
</code></pre>
<p>接下来你可以通过公网ip 端口访问你内网的服务啦。</p></div><p style="text-align:right"><a href="https://dryice.icu/posts/technology/frp#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://dryice.icu/posts/technology/frp</link><guid isPermaLink="true">https://dryice.icu/posts/technology/frp</guid><dc:creator><![CDATA[ice]]></dc:creator><pubDate>Tue, 23 Dec 2025 03:44:52 GMT</pubDate></item><item><title><![CDATA[Ollama离线安装模型方案]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.dryice.icu/images/2025/12/09/20251209114612902_repeat_1765251974693__396336.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/12/09/20251209115244084_repeat_1765252365712__354341.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://dryice.icu/posts/technology/ollama-models-import">https://dryice.icu/posts/technology/ollama-models-import</a></blockquote><div><ol start="1"><li><p><strong>生成Modelfile</strong></p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># 后面模型名称是你要导出的模型名称，这里以qwen3:0.6b为例
ollama show --modelfile qwen3:0.6b &gt;&gt; Modelfile
</code></pre><p>运行后，一个名为 <strong>Modelfile</strong> 的文本文件将在当前目录下生成。</p></li><li><p>确认模型 GGUF 文件路径</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">ollama show --modelfile qwen3:0.6b
#or
cat Modelfile
</code></pre><p>此命令的输出会显示 <strong>Modelfile</strong> 的内容，其中包含模型的 <strong>GGUF</strong> 文件路径，如下图所示
<img src="https://img.dryice.icu/images/2025/12/09/20251209114612902_repeat_1765251974693__396336.png" alt="image.png" height="740" width="1510"/></p></li></ol><ol start="3"><li><p>拷贝模型 GGUF 文件
将上一步确认的 GGUF 文件拷贝到当前工作目录，并给它一个易于识别的名称（例如 <code>qwen3-0.6b.gguf</code>）。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># # 请将 &#x27;/var/lib/ollama/blobs/...&#x27; 替换为你实际查看到的路径
cp  /var/lib/ollama/blobs/sha256-7f4030143c1c477224c5434f8272c662a8b042079a0a584f0a27a1684fe2e1fa ./qwen3-0.6b.gguf
</code></pre>
</li><li><p>编辑<code>Modelfile</code>文件
打开并编辑 <strong>Modelfile</strong> 文件，将 <code>FROM</code> 行的模型路径修改为我们刚刚拷贝到当前目录的相对路径。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">vim Modelfile
</code></pre>
</li></ol><p>将 <strong>Modelfile</strong> 中的内容（例如：<code>FROM /var/lib/ollama/blobs/...</code>）修改为：</p><pre class=""><code class="">FROM ./qwen3-0.6b.gguf # ... 其他配置项保持不变
</code></pre><p><img src="https://img.dryice.icu/images/2025/12/09/20251209115244084_repeat_1765252365712__354341.png" alt="image.png"/></p><ol start="5"><li><p>迁移和部署
将包含<strong>修改后的 Modelfile</strong> 和 <strong>qwen3-0.6b.gguf</strong> 的<strong>整个目录</strong>拷贝到目标（需要安装的）电脑上。
在目标电脑上，进入该目录并运行以下命令创建模型：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># &#x27;qwen3:0.6b&#x27; 是你给新模型自定义的名称和标签
ollama create qwen3:0.6b -f Modelfile
</code></pre><p>模型创建成功后，即可使用 <code>ollama list </code> 查看是否导入成功。</p></li><li><p>自动化脚本（可选）</p><ul><li>Linux/macOS 脚本 (<code>create.sh</code>)
<code>bash
# 设置模型名称和标签
MODEL_NAME=&quot;qwen3:0.6b&quot;
echo &quot;开始创建 Ollama 模型: $MODEL_NAME&quot;
ollama create $MODEL_NAME -f Modelfile
if [ $? -eq 0 ]; then
echo &quot;模型 $MODEL_NAME 创建成功！&quot;
else
echo &quot;模型 $MODEL_NAME 创建失败。&quot;
fi
</code></li></ul></li></ol><ul><li>Windows 脚本 (<code>create.bat</code>)
<code>bash
@echo off
set MODEL_NAME=qwen3:0.6b
echo 正在创建 Ollama 模型: %MODEL_NAME%
ollama create %MODEL_NAME% -f Modelfile
if %errorlevel% equ 0 (
  echo.
  echo 模型 %MODEL_NAME% 创建成功！
) else (
  echo.
  echo 模型 %MODEL_NAME% 创建失败。
)
pause
</code></li></ul></div><p style="text-align:right"><a href="https://dryice.icu/posts/technology/ollama-models-import#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://dryice.icu/posts/technology/ollama-models-import</link><guid isPermaLink="true">https://dryice.icu/posts/technology/ollama-models-import</guid><dc:creator><![CDATA[ice]]></dc:creator><pubDate>Tue, 09 Dec 2025 08:42:38 GMT</pubDate></item><item><title><![CDATA[Spring AI 学习笔记]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://dryice.icu/posts/technology/spring-ai-study">https://dryice.icu/posts/technology/spring-ai-study</a></blockquote><div><h2 id="">前期准备</h2><ul><li>本文所需的环境和平台
&gt;jdk 21
&gt; 
&gt;node 22.21.1
&gt; 
&gt;ollama server (本地服务器均可)
&gt; 
&gt;阿里百炼平台api-key (避免明文使用，建议加入到环境变量)
&gt; 
&gt;向量数据库 redis-stack (拥有redis所有功能 拓展了向量存储功能)
&gt; 
&gt;向量数据库 pgvector (拥有pgsql所有功能 拓展了向量存储功能)</li></ul><ul><li><p>pgvector docker 快速安装</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">docker run -d \
--name pgvector \
--restart unless-stopped \
-p 5432:5432 \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-v ./data/postgresql:/var/lib/postgresql/data \
pgvector/pgvector:pg16
</code></pre><p>或者 docker compose</p><pre class="language-yml lang-yml"><code class="language-yml lang-yml">services:
pgvector:
  image: pgvector/pgvector:pg16
  container_name: pgvector
  restart: unless-stopped
  ports:
    - &quot;5432:5432&quot;
  volumes:
    - ./data/postgresql:/var/lib/postgresql/data
    #- ./conf/postgresql.conf:/var/lib/postgresql/data/postgresql.conf
    #- ./conf/pg_hba.conf:/var/lib/postgresql/data/pg_hba.conf
  environment:
    # POSTGRES_DB: postgres
    POSTGRES_USER: postgres
    POSTGRES_PASSWORD: postgres
</code></pre>
</li><li><p>redis-stack docker 快速安装</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">docker run -d \
--name redis-stack \
--restart unless-stopped \
-p 6380:6379 \
-p 18001:8001 \
-v ./data/redis:/data \
redis/redis-stack:latest
</code></pre><p>或者docker compose</p><pre class="language-yml lang-yml"><code class="language-yml lang-yml">services:
redis-stack:
  image: redis/redis-stack:latest
  container_name: redis-stack
  restart: unless-stopped
  ports:
    - &quot;6380:6379&quot; 
    - &quot;18001:8001&quot; 
  volumes:
    - ./data/redis:/data
    # - ./config/redis-stack.conf:/redis-stack.conf:ro
  # command: [&quot;redis-server&quot;, &quot;/redis-stack.conf&quot;]
  healthcheck:
    test: [&quot;CMD&quot;, &quot;redis-cli&quot;, &quot;ping&quot;]
    interval: 10s
    timeout: 5s
    retries: 3
</code></pre>
</li></ul><h2 id="">一、基础问答</h2><p>这里的基础问答指的是 <strong>无状态</strong> 的、一次性的问答交互。
在这种模式下，每一次用户请求都被视为一个独立的会话，模型在处理当前问题时，不会保留或记住之前任何一次交互的内容和上下文。</p><h3 id="11-">1.1 依赖</h3><p>基础对话所需依赖项目</p><pre class="language-xml lang-xml"><code class="language-xml lang-xml">    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;!--spring-ai-alibaba dashscope--&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud.ai&lt;/groupId&gt;
            &lt;artifactId&gt;spring-ai-alibaba-starter-dashscope&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;!--ollama--&gt;
        &lt;!--
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
            &lt;artifactId&gt;spring-ai-starter-model-ollama&lt;/artifactId&gt;
            &lt;version&gt;1.0.0&lt;/version&gt;
        &lt;/dependency&gt;
        --&gt;
        &lt;!--lombok--&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
            &lt;artifactId&gt;lombok&lt;/artifactId&gt;
            &lt;optional&gt;true&lt;/optional&gt;
        &lt;/dependency&gt;
        &lt;!--hutool--&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;cn.hutool&lt;/groupId&gt;
            &lt;artifactId&gt;hutool-all&lt;/artifactId&gt;
            &lt;version&gt;5.8.22&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
            &lt;scope&gt;test&lt;/scope&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;
</code></pre>
<h3 id="12-api-key">1.2 配置api key</h3><p>获取api key ： <a href="https://bailian.console.aliyun.com/?apiKey=1&amp;tab=api#/api">获取地址</a></p><pre class="language-yml lang-yml"><code class="language-yml lang-yml">spring:
  datasource:
    url: jdbc:postgresql://127.0.0.1:5432/postgres
    username: postgres
    password: postgres
    driver-class-name: org.postgresql.Driver
  application:
    name: chat-memory
  data:
    redis:
      host: localhost
      port: 6379
      password:
      database: 1
      connect-timeout: 3
      timeout: 3
  ai:
    # ====SpringAIAlibaba Config=============
    dashscope:
      # 必选 配置环境变量 DASH_SCOPE_API_KEY=你的apiky
      api-key: ${DASH_SCOPE_API_KEY}
      # 可选 
      base-url: &quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;
      # 可选，可在config中配置多个模型
      chat:
        options:
          model: &quot;qwen-plus-2025-09-11&quot;
     # ====SpringAI Config=============
    #openai:
      #api-key: ${OPENAI_API_KEY}
      #base-url: &quot;https://api.openai.com/v1&quot;
      #chat:
        #ptions:
          #model: &quot;gpt-3.5-turbo&quot;
</code></pre>
<h3 id="13-chatmodel-">1.3 ChatModel 方式</h3><p><code>ChatModel</code> 是 Spring AI 提供的最低级别、最基础的抽象接口，用于与底层大语言模型（LLM）进行交互。</p><h4 id="131-chatmodel">1.3.1 创建ChatModel</h4><pre class="language-java lang-java"><code class="language-java lang-java">    @Bean
    public DashScopeApi dashScopeApi() {
        return DashScopeApi.builder()
                .apiKey(System.getenv(&quot;DASH_SCOPE_API_KEY&quot;))
                .build();
    }
</code></pre><h4 id="132-">1.3.2 基础使用</h4><p>使用 <code>ChatModel</code> 实现消息的流式返回和阻塞返回</p><pre class="language-java lang-java"><code class="language-java lang-java">@RestController
@RequestMapping(&quot;/chat&quot;)
public class ChatHelloController {
    @Resource
    private ChatModel chatModel;
    @GetMapping(value = &quot;/block&quot;)
    public String doChat(@RequestParam(name = &quot;question&quot;, defaultValue = &quot;你是谁&quot;) String question) {
        return chatModel.call(question);
    }
    @GetMapping(value = &quot;/stream&quot;)
    public Flux&lt;String&gt; stream(@RequestParam(name = &quot;question&quot;, defaultValue = &quot;你是谁&quot;) String question) {
        return chatModel.stream(question);
    }
}
</code></pre>
<h3 id="14-chatclient-">1.4 ChatClient 使用</h3><p><code>ChatClient</code> 是 Spring AI 应用程序中最常用的交互接口。它在底层 <code>ChatModel</code> 之上提供了易用性、集成性和扩展性，让开发者能够专注于业务逻辑，而不是底层 API 的细节。</p><h4 id="141-">1.4.1 官方解释</h4><p><code>ChatClient</code> 提供了与 AI 模型通信的 Fluent API，它支持同步和反应式（Reactive）编程模型。与 <code>ChatModel</code>、<code>Message</code>、<code>ChatMemory</code> 等原子 API 相比，使用 <code>ChatClient</code> 可以将与 LLM 及其他组件交互的复杂性隐藏在背后，因为基于 LLM 的应用程序通常要多个组件协同工作（例如，提示词模板、聊天记忆、LLM Model、输出解析器、RAG 组件：嵌入模型和存储），并且通常涉及多个交互，因此协调它们会让编码变得繁琐。当然使用 <code>ChatModel</code> 等原子 API 可以为应用程序带来更多的灵活性，成本就是您需要编写大量样板代码。
ChatClient 类似于应用程序开发中的服务层，它为应用程序直接提供 <code>AI 服务</code>，开发者可以使用 ChatClient Fluent API 快速完成一整套 AI 交互流程的组装。</p><p>包括一些基础功能，如：</p><ul><li>定制和组装模型的输入（Prompt）</li><li>格式化解析模型的输出（Structured Output）</li><li>调整模型交互参数（ChatOptions）</li></ul><p>还支持更多高级功能：</p><ul><li>聊天记忆（Chat Memory）</li><li>工具/函数调用（Function Calling）</li><li>RAG</li></ul><h4 id="142-chatclient">1.4.2 创建ChatClient</h4><p><code>ChatClient</code> 默认情况下不会被 Spring Boot 自动配置为可直接 <code>@Autowired</code> 或 <code>@Resource</code> 注入的 <code>Bean</code>。 这是因为 <code>ChatClient</code> 需要通过其 <code>Builder</code> 模式来构建，并且必须依赖于一个或多个底层 <code>ChatModel</code> 实例。
有两种注入方式</p><ol start="1"><li><p>自定义 <code>@Bean</code> (推荐方式)</p><p>这是最常用且最符合 Spring 规范的做法。通过定义一个 <code>@Bean</code> 方法，您可以清晰地指定 <code>ChatClient</code> 依赖于哪个具体的 <code>ChatModel</code>（例如，这里依赖于 <code>dashscopeChatModel</code>），并可以在构建时应用默认配置（如默认系统指令、默认工具等）。</p><pre class="language-java lang-java"><code class="language-java lang-java">@Bean
public ChatClient chatClient(ChatModel dashscopeChatModel) {
 return ChatClient.builder(dashscopeChatModel).build();
}
</code></pre>
</li><li><p>构造方法注入依赖并手动构建</p><p>这种方式适用于您只想在特定组件中使用 <code>ChatClient</code>，而不希望将其暴露为全局 <code>Bean</code> 的场景。您可以在组件的构造函数中注入所需的 <code>ChatModel</code>，然后手动完成 <code>ChatClient</code> 的构建。</p><pre class="language-java lang-java"><code class="language-java lang-java">private final ChatClient dashScopeChatClient;
public ChatClientController(ChatModel dashScopeChatModel) {
 this.dashScopeChatClient = ChatClient.builder(dashScopeChatModel).build();
}
</code></pre>
</li></ol><h4 id="143-">1.4.3 基础使用</h4><p>使用 <code>ChatClient</code> 实现消息的流式返回和阻塞返回</p><pre class="language-java lang-java"><code class="language-java lang-java">@Resource
private ChatClient dashScopeChatClient;
@GetMapping(&quot;/client&quot;)
public String chatClientBlock(@RequestParam(name = &quot;question&quot;, defaultValue = &quot;2加9等于几&quot;) String question) {
    return dashScopeChatClient.prompt().user(question).call().content();
}
@GetMapping(&quot;/client&quot;)
public Flux&lt;String&gt; chatClientStream(@RequestParam(name = &quot;question&quot;, defaultValue = &quot;2加9等于几&quot;) String question) {
    return dashScopeChatClient.prompt().user(question).stream().content();
}
</code></pre>
<h3 id="15-ollama-">1.5 使用本地模型(ollama) 实现基础对话</h3><p>选择本地模型是为了在数据隐私、离线运行、长期免费和低延迟方面取得优势，特别适用于不能联网或对敏感数据有严格要求的项目。</p><h4 id="151-">1.5.1 引入依赖</h4><pre class="language-xml lang-xml"><code class="language-xml lang-xml">&lt;!--ollama--&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
    &lt;artifactId&gt;spring-ai-starter-model-ollama&lt;/artifactId&gt;
    &lt;version&gt;1.0.0&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<h4 id="152-">1.5.2 编辑配置</h4><pre class="language-yml lang-yml"><code class="language-yml lang-yml">spring:
  application:
    name: ollama
  ai:
    ollama:
      base-url: &quot;http://192.168.187.166:11434&quot;
      chat:
        model: &quot;qwen3:8b&quot;
</code></pre>
<h4 id="153-">1.5.3 基础使用</h4><p><code>ChatModel</code> &amp;&amp; <code>ChatClient</code> 流试阻塞方式调用。</p><pre class="language-java lang-java"><code class="language-java lang-java">    @Resource  
    private ChatModel chatModel;  
      
    public final ChatClient chatClient;  
    public OllamaController(ChatClient.Builder builder) {  
        this.chatClient = builder.build();  
    }

    // ChatModel 方式
    @GetMapping(&quot;/stream&quot;)
    public Flux&lt;String&gt; stream(@RequestParam(name = &quot;question&quot;, defaultValue = &quot;你是谁&quot;) String question) {
        // 阻塞方式
        //return chatModel.call(question);
        // 流式方式
        return chatModel.stream(question);
    }
    
    // ChatClient方式
    @GetMapping(&quot;/client&quot;)
    public Flux&lt;String&gt; client(@RequestParam(name = &quot;question&quot;, defaultValue = &quot;你是谁&quot;) String question) {
        // 阻塞方式
        //return chatClient.prompt().user(question).call().content();
        // 流式方式
        return chatClient.prompt().user(question).stream().content();
    }
</code></pre><h3 id="16-">1.6 多模型共存</h3><p>多模型共存是为了实现成本、性能和合规性的最佳平衡，允许项目根据任务需求灵活选择和切换本地、线上或不同厂商的模型。</p><p><code>ChatClient</code> 通过 <code>ChatModel</code> 创建，<code>ChatModel</code>（对于spring-ai-alibaba来说）也可通过<code>DashScopeChatModel</code> 创建不同模型的<code>ChatModel</code>，外部的依赖也可以引入相关的<code>ChatModel</code>如<code>ollama</code>。 
这样我门就可以在一个项目中使用多个模型。下面是示例：</p><pre class="language-java lang-java"><code class="language-java lang-java">@Configuration
public class SaaLLMConfig {
    
    private final String DEEPSEEK_MODEL = &quot;deepseek-v3.1&quot;;
    private final String QWEN_MODEL = &quot;qwen-plus-2025-09-11&quot;;
    
    @Bean(name = &quot;deepseek&quot;)
    public ChatModel deepSeek() {
        return DashScopeChatModel.builder()
                .dashScopeApi(DashScopeApi.builder().apiKey(System.getenv(&quot;DASH_SCOPE_API_KEY&quot;)).build())
                .defaultOptions(DashScopeChatOptions.builder().withModel(DEEPSEEK_MODEL).build())
                .build();
    }

    @Bean(name = &quot;qwen&quot;)
    public ChatModel qwen() {
        return DashScopeChatModel.builder()
                .dashScopeApi(DashScopeApi.builder().apiKey(System.getenv(&quot;DASH_SCOPE_API_KEY&quot;)).build())
                .defaultOptions(DashScopeChatOptions.builder().withModel(QWEN_MODEL).build())
                .build();
    }

    @Bean(name = &quot;deepseekChatClient&quot;)
    public ChatClient deepseekChatClient(@Qualifier(&quot;deepseek&quot;) ChatModel deepseek) {
        return ChatClient.builder(deepseek)
                        .defaultOptions(ChatOptions.builder().model(DEEPSEEK_MODEL).build())
                        .build();
    }

    @Bean(name = &quot;qwenChatClient&quot;)
    public ChatClient qwenChatClient(@Qualifier(&quot;qwen&quot;) ChatModel qwen) {
        return ChatClient.builder(qwen)
                        .defaultOptions(ChatOptions.builder().model(QWEN_MODEL).build())
                        .build();
    }

    @Bean(name = &quot;ollamaChatClient&quot;)
    public ChatClient ollamaChatClient(@Qualifier(&quot;ollamaChatModel&quot;) ChatModel ollama) {
        return ChatClient.builder(ollama)
                .defaultOptions(ChatOptions.builder().model(&quot;qwen3:8b&quot;).build())
                .build();
    }
}
</code></pre>
<h2 id="">二、记忆存储</h2><p><strong>记忆存储</strong>是为了打破基础对话的<strong>无状态限制</strong>，使 AI Agent 能够<strong>记住先前交互的上下文</strong>，从而实现连贯且自然的<strong>多轮对话</strong>。</p><hr/><p>这里主要介绍 <strong>redis</strong> 和 <strong>postgres</strong> 两种方式作为记忆存储方式，其他类型的存储方式请参考 spring-ai 文档，这不需要向量库，普通的redis和postgres就可以。</p><h3 id="21-">2.1 引入依赖</h3><pre class="language-xml lang-xml"><code class="language-xml lang-xml">    &lt;!--spring-ai-alibaba dashscope--&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;com.alibaba.cloud.ai&lt;/groupId&gt;
        &lt;artifactId&gt;spring-ai-alibaba-starter-dashscope&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!-- redis 存储所需依赖 经过spring ai alibaba 又封装了一层 --&gt;
    &lt;!--spring-ai-alibaba memory-redis--&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;com.alibaba.cloud.ai&lt;/groupId&gt;
        &lt;artifactId&gt;spring-ai-alibaba-starter-memory-redis&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!--jedis--&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;redis.clients&lt;/groupId&gt;
        &lt;artifactId&gt;jedis&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!--  postgresql 存储所需依赖 没有封装的 spring ai 原生支持的 --&gt;
    &lt;!--spring-ai memory-jdbc--&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
        &lt;artifactId&gt;spring-ai-starter-model-chat-memory-repository-jdbc&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.postgresql&lt;/groupId&gt;
        &lt;artifactId&gt;postgresql&lt;/artifactId&gt;
        &lt;version&gt;42.5.0&lt;/version&gt;
    &lt;/dependency&gt;
</code></pre>
<h3 id="22-">2.2 添加配置</h3><pre class="language-yml lang-yml"><code class="language-yml lang-yml">spring:
  datasource:
    url: jdbc:postgresql://127.0.0.1:5432/postgres
    username: postgres
    password: postgres
    driver-class-name: org.postgresql.Driver
  application:
    name: chat-memory
  data:
    redis:
      host: localhost
      port: 6379
      password:
      database: 1
      connect-timeout: 3
      timeout: 3
  ai:
    chat:
      memory:
        repository:
          jdbc:
            initialize-schema: always
</code></pre>
<h3 id="23-bean">2.3 构造Bean</h3><p>文档中主要介绍ollama + Jdbc存储方式，其他方式chat-memory模块中有示例代码。</p><h4 id="231--jdbc-postgresmemoryconfig">2.3.1 配置 JDBC 聊天记忆仓库（PostgresMemoryConfig）</h4><p> 这个配置类负责创建并注册 <code>JdbcChatMemoryRepository</code> 实例，它依赖于 Spring Boot 自动配置的 <code>JdbcTemplate</code> 来与 PostgreSQL 数据库交互。</p><pre class="language-java lang-java"><code class="language-java lang-java">@Configuration
public class PostgresMemoryConfig {
    // 依赖注入：通过构造函数获取 JdbcTemplate
    private final JdbcTemplate jdbcTemplate;
    // 推荐使用构造函数注入，确保依赖不可变
    public PostgresMemoryConfig(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    /**
     * 定义 JdbcChatMemoryRepository Bean。
     * 它是 ChatMemoryRepository 接口的实现，负责将聊天记录存储到数据库中。
     *
     * @return 配置好的 JdbcChatMemoryRepository 实例
     */
    @Bean
    // 标记为 @Primary 以解决可能的歧义，确保在需要默认 ChatMemoryRepository 时优先选择 JDBC 实现。
    @Primary
    public JdbcChatMemoryRepository jdbcChatMemoryRepository() {
        return JdbcChatMemoryRepository.builder()
                // 注入之前获取的 JdbcTemplate
                .jdbcTemplate(jdbcTemplate)
                .build();
    }
}
</code></pre><p><strong>关键点解释：</strong></p><ul><li><code>@Primary</code>: 该注解用于解决歧义性。当应用中存在多个 <code>ChatMemoryRepository</code> 接口的实现（例如，同时存在 <code>JDBC</code> 和 <code>Redis</code> 实现）时，<code>@Primary</code>告诉 Spring，这是默认或首选的实现。</li></ul><h4 id="232--chatclientsaallmconfig">2.3.2 集成到 ChatClient（SaaLLMConfig）</h4><p>   这个配置类展示了如何将上一步定义的聊天记忆仓库连接到特定的 ChatModel（此处为 Ollama），从而为该模型提供有状态（带记忆）的会话能力。</p><pre class="language-java lang-java"><code class="language-java lang-java">@Configuration
public class SaaLLMConfig {
    private final String OLLAMA_MODEL = &quot;qwen3:8b&quot;;
    /**
     * 定义 Ollama 聊天客户端 Bean，并集成 JDBC 聊天记忆。
     *
     * @param ollamaChatModel 依赖注入的 Ollama ChatModel 实例 (使用 @Qualifier 区分)
     * @param jdbcChatMemoryRepository 依赖注入的 JDBC 聊天记忆仓库
     * @return 配置了记忆功能的 ChatClient 实例
     */
    @Bean(name = &quot;ollamaChatClient&quot;)
    public ChatClient ollamaChatClient(@Qualifier(&quot;ollamaChatModel&quot;) ChatModel ollamaChatModel,
                                       JdbcChatMemoryRepository jdbcChatMemoryRepository) {
        // 1. 构造 MessageWindowChatMemory（消息窗口记忆）
        // 它负责维护一个固定大小（窗口）的对话历史。
        MessageWindowChatMemory windowChatMemory = MessageWindowChatMemory
                .builder()
                // 关联具体的持久化仓库（这里是 JDBC）
                .chatMemoryRepository(jdbcChatMemoryRepository)
                // 设置最大消息数，超过此限制旧消息将被移除
                .maxMessages(100)
                .build();
        // 2. 构造 ChatClient
        return ChatClient
                .builder(ollamaChatModel)
                // 设置默认模型选项
                .defaultOptions(ChatOptions.builder().model(OLLAMA_MODEL).build())
                // 添加 MessageChatMemoryAdvisor，它是实现记忆功能的核心组件
                // Advisor 会在每次请求时读取历史记录，并在响应后写入新记录。
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(windowChatMemory).build())
                .build();
    }
}
</code></pre><p><strong>关键组件解释：</strong></p><ul><li><p><strong><code>MessageWindowChatMemory</code></strong>: <strong>记忆策略</strong>。它定义了如何管理对话历史，例如只保留最近的 N 条消息（此处为 100 条）。</p></li><li><p><strong><code>jdbcChatMemoryRepository</code></strong>: <strong>记忆仓库 (Repository)</strong>。它是 <code>MessageWindowChatMemory</code> 实际存储和读取数据的地方（PostgreSQL 数据库）。</p></li><li><p><strong><code>MessageChatMemoryAdvisor</code></strong>: <strong>AOP 切面/建议器</strong>。它是 Spring AI 机制的核心，负责在 ChatModel 调用前后自动执行记忆的<strong>读取</strong>和<strong>写入</strong>操作，从而将无状态的模型调用转化为有状态的对话。</p></li></ul><h3 id="24-">2.4 记忆对话交互</h3><p>为了确保多用户和多会话之间记忆的隔离性和准确性，我们需要在每次调用时，向 <code>ChatClient</code> 明确传递一个唯一的会话标识符。Spring AI 通过 <code>ConversationHistoryAdvisor</code>（在底层自动启用）和 <code>CONVERSATION_ID</code> 参数来实现这一点。</p><pre class="language-java lang-java"><code class="language-java lang-java">@RestController
@RequestMapping(&quot;/memory&quot;)
public class ChatMemoryController {
    @Resource(name = &quot;ollamaChatClient&quot;)
    private ChatClient ollamaChatClient;
    @GetMapping(&quot;/chat3&quot;)
    public Flux&lt;String&gt; ollamaMemory(@RequestParam(name = &quot;question&quot;, defaultValue = &quot;1+1等于几&quot;) String question,
                                     String userId,
                                     String conversationId) {
        return ollamaChatClient.prompt(question).advisors(
                        // 构造唯一的会话标识符
                        advisorSpec -&gt; advisorSpec.param(CONVERSATION_ID, userId + &quot;-&quot; + conversationId))
                .stream().content();
    }
}

</code></pre>
<h3 id="25-">2.5 其他记忆方式说明</h3><p>除了 JDBC 外，chat-memory 模块还支持其他几种持久化方式。
除了 JDBC 外，<code>chat-memory</code> 模块还支持其他几种持久化方式。您可以参考项目中的示例代码来配置：</p><table><thead><tr><th><strong>记忆仓库</strong></th><th><strong>描述</strong></th><th><strong>适用场景</strong></th></tr></thead><tbody><tr><td><strong><code>RedisChatMemoryRepository</code></strong></td><td>使用 Redis 存储。</td><td>高并发、需要低延迟读写、数据量适中。</td></tr><tr><td><strong><code>InMemoryChatMemoryRepository</code></strong></td><td>数据存储在应用程序内存中。</td><td>测试、开发环境；不适合生产环境（数据不持久化）。</td></tr></tbody></table><p>您只需要替换 <code>jdbcChatMemoryRepository</code> 为相应的实现，并在其 Bean 上添加 <code>@Primary</code> (如果需要) 即可</p><h2 id="">三、提示词</h2><p>提示词（Prompt）是与大语言模型（LLM）交互的核心载体和指令集。它本质上是提供给模型的一段输入文本，用于引导模型执行特定的任务、遵循特定的规则，并产生期望的输出。</p><p>在 Spring AI 中，与大型语言模型（LLM）的交互是通过 <strong><code>Prompt</code></strong> 对象完成的，而 <code>Prompt</code> 内部由一系列不同角色的 <strong>消息 (Message)</strong> 组成。掌握这些消息类型是精确控制模型行为的关键。</p><h3 id="31-">3.1 消息类型</h3><table><thead><tr><th> <strong>消息类型</strong>               </th><th> <strong>角色 (Role)</strong>  </th><th> <strong>目的</strong>                                     </th><th> <strong>使用场景</strong>                                                                                                                                    </th></tr></thead><tbody><tr><td> <strong><code>SystemMessage</code></strong>    </td><td> System (系统)    </td><td> 设定模型的<strong>身份、行为、约束</strong>和<strong>通用指令</strong>。它定义了 LLM 的“人设”。 </td><td> <strong>设定角色：</strong> “你是一个法律助手”。 <strong>格式要求：</strong> “请以 JSON 格式返回”。                                                                                             </td></tr><tr><td> <strong><code>UserMessage</code></strong>      </td><td> User (用户)      </td><td> 包含用户的<strong>实际问题、请求或输入</strong>。                       </td><td> 用户的提问、指令。                                                                                                                                   </td></tr><tr><td> <strong><code>AssistantMessage</code></strong> </td><td> Assistant (助手) </td><td> 包含模型<strong>历史的回复</strong>。                             </td><td> 用于在构建 <code>Prompt</code> 时，手动加入历史对话（通常由 <code>ChatMemory</code> 自动管理）。                                                                                           </td></tr><tr><td> <strong><code>ToolMessage</code></strong>        </td><td> Tool (工具)      </td><td> 包含 LLM 请求调用的<strong>外部工具执行后的结果或输出</strong>。             </td><td> LLM 决定调用一个函数（例如：<code>get_weather(location)</code>）。<br/>应用执行该函数得到结果（例如：<code>{&quot;temperature&quot;: &quot;25°C&quot;}</code>）。<br/><strong><code>ToolMessage</code></strong> 将这个结果反馈给 LLM，让其基于此结果生成最终的用户回复。 </td></tr></tbody></table><h3 id="32-">3.2 使用示例</h3><p>下面分别是 <code>ChatClient</code> 和 <code>ChatModel</code> 的使用方式：</p><pre class="language-java lang-java"><code class="language-java lang-java">@RestController
@RequestMapping(&quot;/prompt&quot;)
public class PromptController {
    @Resource(name = &quot;deepseek&quot;)
    private ChatModel deepseekChatModel;
    
    @Resource(name = &quot;deepseekChatClient&quot;)
    private ChatClient deepseekChatClient;

    @GetMapping(&quot;/law&quot;)
    public Flux&lt;String&gt; law(@RequestParam(name = &quot;question&quot;, defaultValue = &quot;你是谁&quot;) String question) {
        return deepseekChatClient.prompt()
                // 能力边界
                .system(&quot;你是一个法律助手，只回答法律问题，其它问题回复，我只能回答法律相关问题，其它无可奉告&quot;)
                .user(question)
                .stream()
                .content();
    }
    @GetMapping(&quot;/story/html&quot;)
    public Flux&lt;String&gt; storyHtml(@RequestParam(name = &quot;question&quot;, defaultValue = &quot;讲个故事&quot;) String question) {
        // 系统消息
        SystemMessage systemMessage = new SystemMessage(&quot;你是一个讲故事的助手,每个故事控制在600字以内且以HTML格式返回&quot;);
        // 用户消息
        UserMessage userMessage = new UserMessage(question);
        Prompt prompt = new Prompt(userMessage, systemMessage);
        return deepseekChatModel.stream(prompt)
                .mapNotNull(response -&gt;
                        response.getResults().getFirst().getOutput().getText()
                );
    }
}
</code></pre>
<h3 id="33-">3.3 提示词模板</h3><p><strong>提示词模板</strong>是 Spring AI 中用于创建动态、可复用提示词的核心工具。它允许开发者将固定的指令（如角色设定、格式要求）与动态的运行时参数（如用户输入、主题、字数）结合起来，从而灵活地构造出发送给 LLM 的 <code>Prompt</code> 对象。</p><table><thead><tr><th> <strong>类名</strong>                         </th><th> <strong>描述</strong>                                               </th><th> <strong>作用</strong>                                   </th></tr></thead><tbody><tr><td> <strong><code>PromptTemplate</code></strong>           </td><td> 最基础的模板类，用于构造 <strong><code>UserMessage</code></strong> 或简单的<strong>单条 <code>Message</code></strong>。 </td><td> 接收一个包含占位符（如 <code>{topic}</code>）的字符串，并根据 Map 参数填充。 </td></tr><tr><td> <strong><code>SystemPromptTemplate</code></strong>     </td><td> 专用于构造 <strong><code>SystemMessage</code></strong> 的模板类。                      </td><td> 确保生成的 <code>Message</code> 具有系统角色的语义，用于设置模型身份或约束。   </td></tr><tr><td> <strong><code>@Value(&quot;classpath:/...&quot;)</code></strong> </td><td> Spring Resource Loader。                              </td><td> 允许将复杂的提示词内容存储在外部文件（如 <code>.txt</code>）中，使代码更清晰。    </td></tr></tbody></table><h4 id="331-">3.3.1 使用示例</h4><h5 id="3311-">3.3.1.1 字符串内联模板</h5><pre class="language-java lang-java"><code class="language-java lang-java">public Flux&lt;String&gt; story() {
    // 模板定义在代码中
    PromptTemplate promptTemplate = new PromptTemplate(
                     &quot;讲一个关于{topic}的故事&quot; +
                    &quot;并以{output_format}格式输出，&quot; +
                    &quot;字数在{word_count}左右&quot;);
    // 创建 Prompt 对象
    Prompt prompt = promptTemplate.create(Map.of(
            &quot;topic&quot;, topic,
            &quot;output_format&quot;, outputFormat,
            &quot;word_count&quot;, wordCount));
    // 流式调用
    return deepseekChatClient.prompt(prompt).stream().content();
}
</code></pre><ul><li><p><strong>特点：</strong> 最直接的使用方式，适用于模板内容较短的场景。</p></li><li><strong>调用方式：</strong> 使用 <code>ChatClient.prompt(Prompt)</code> 进行流式调用 (<code>.stream().content()</code>)，简洁高效。
##### 3.3.1.2 外部文件模板
此示例展示了如何将模板内容外置到文件 (<code>template.txt</code>) 中，并通过 Spring Resource 加载。
<code>java
@Value(&quot;classpath:/prompt-template/template.txt&quot;)
private org.springframework.core.io.Resource userTemplate;
public Flux&lt;String&gt; story() {
  // 使用 Resource 构造 PromptTemplate
  PromptTemplate promptTemplate = new PromptTemplate(userTemplate);
  // 创建 Prompt 对象
  Prompt prompt = promptTemplate.create(Map.of(&quot;topic&quot;, topic, &quot;output_format&quot;, outputFormat, &quot;word_count&quot;, wordCount));
  // 流式调用 (使用 ChatModel)
  return deepseekChatModel.stream(prompt).mapNotNull(
          chatResponse -&gt; chatResponse.getResult().getOutput().getText()
  );
}
</code></li><li><p><strong>特点：</strong> 模板内容（如故事格式、字数要求）存储在外部文件，便于维护和修改，代码更整洁。</p></li><li><p><strong>调用方式：</strong> 使用 <code>ChatModel.stream(Prompt)</code>，需要手动通过 <code>.mapNotNull(...)</code> 提取文本内容。</p><h5 id="3313---">3.3.1.3 系统模板 + 用户模板</h5><p>此示例是最完整的提示词构建模式，它分离了系统指令和用户输入，分别使用对应的模板类构建消息，最后组合成一个 <code>Prompt</code>。</p><pre class="language-java lang-java"><code class="language-java lang-java">public Flux&lt;String&gt; story() {
  // 1. SystemPromptTemplate：创建 SystemMessage
  SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(&quot;你是{system_topic}助手，只回答{system_topic}其它无可奉告，以HTML格式的结果。&quot;);
  Message sysMessage = systemPromptTemplate.createMessage(Map.of(&quot;system_topic&quot;, sysTopic));
  // 2. PromptTemplate：创建 UserMessage
  PromptTemplate userPromptTemplate = new PromptTemplate(&quot;解释一下{user_topic}&quot;);
  Message userMessage = userPromptTemplate.createMessage(Map.of(&quot;user_topic&quot;, userTopic));
  // 3. 组合：将 SystemMessage 和 UserMessage 组合成 Prompt
  Prompt prompt = new Prompt(List.of(sysMessage, userMessage));
  // 4. 调用 LLM
  return deepseekChatModel.stream(prompt).mapNotNull(
          chatResponse -&gt; chatResponse.getResult().getOutput().getText()
  );
}
</code></pre>
</li></ul><h2 id="">四、结构化输出</h2><p><strong>结构化输出</strong>功能允许开发者定义 Java 对象（如 <code>Record</code> 或 POJO）的结构，并让 LLM 的回复直接映射到该对象实例上。这通过将 JSON Schema 注入到提示词中实现，有效地将 LLM 的文本生成任务转化为<strong>数据生成任务</strong></p><h3 id="41-">4.1 核心机制与优势</h3><table><thead><tr><th> <strong>机制</strong>             </th><th> <strong>描述</strong>                                                                                                   </th><th> <strong>优势</strong>                          </th></tr></thead><tbody><tr><td> <strong>JSON Schema 注入</strong> </td><td> Spring AI 库将目标 Java 类（例如 <code>StudentRecord.class</code>）转化为 <strong>JSON Schema</strong> 定义，并将其作为 <code>SystemMessage</code> 的一部分发送给 LLM。 </td><td> 强制模型输出符合该 Schema 的 JSON 字符串。    </td></tr><tr><td> <strong>数据绑定</strong>           </td><td> LLM 返回 JSON 字符串后，Spring AI 自动将该 JSON 字符串反序列化（Deserialization）绑定到您指定的 Java 类实例上。                          </td><td> 无需手动编写 JSON 解析代码，确保数据的类型安全和准确性。 </td></tr><tr><td> <strong><code>Record</code> 类支持</strong>   </td><td> 推荐使用 Java 14+ 的 <code>Record</code> 类型，它们简洁、不可变且自动提供了构造函数和访问器。                                                      </td><td> 简化数据模型的创建。                      </td></tr></tbody></table><h3 id="42-">4.2 使用示例</h3><p><strong>前提：数据模型定义 (StudentRecord)</strong></p><p>假设您有一个用于接收结构化数据的 Java Record 类（或其他 POJO）：</p><pre class="language-java lang-java"><code class="language-java lang-java">// 假设 StudentRecord 包含 LLM 需要填充的字段
public record StudentRecord(
    String id, 
    String name, 
    String major, 
    String email
) {}
</code></pre><pre class="language-java lang-java"><code class="language-java lang-java">@RestController
@RequestMapping(&quot;/struct&quot;)
public class StructureOutputController {
    @Resource(name = &quot;qwenChatClient&quot;)
    private ChatClient qwenChatClient;

    @GetMapping(&quot;/chat2&quot;)
    public StudentRecord chat2(@RequestParam(name = &quot;name&quot;, defaultValue = &quot;王一狗&quot;) String name,
                               @RequestParam(name = &quot;email&quot;, defaultValue = &quot;ice@jlau.com&quot;) String email) {
        String stringTemplate = &quot;&quot;&quot;
                学号1002，我叫{name},大学专业软件工程,邮箱{email}
                &quot;&quot;&quot;;
        return qwenChatClient.prompt()
                .user(
                        promptUserSpec -&gt;
                                // 这里的参数 name 和 email 会被替换成对应的值，但并不是返回的值，具体的值还需靠模型的理解能力。
                                promptUserSpec.text(stringTemplate).param(&quot;name&quot;, name).param(&quot;email&quot;, email)
                        // 这里返回的结果主要靠模型的理解能力，所以结果可能不准确，可以拿下面的例子测试一下。
                        //promptUserSpec.text(&quot;学号001，我是武汉市长江大桥,大学专业计算机科学与技术,邮箱xxx.ice@gmail.com&quot;)
                        //promptUserSpec.text(&quot;学号001，我是李四不对我是张三的哥哥王五的弟弟武汉市长江大桥的哥哥王麻子的叔叔你爸爸,大学专业计算机科学与技术,邮箱xxx.ice@gmail.com&quot;)
                        //promptUserSpec.text(&quot;学号001，我是李四不对我是张三,大学专业计算机科学与技术,邮箱xxx.ice@gmail.com&quot;)
                        //promptUserSpec.text(&quot;学号001，我是李四李四的爸爸李鬼,大学专业计算机科学与技术,邮箱xxx.ice@gmail.com&quot;)
                )
                .call()
                .entity(StudentRecord.class); // 指定返回的结构化 Java 对象类型
    }
}
</code></pre><h3 id="43-">4.3 注意事项</h3><ul><li><strong>模型能力依赖</strong>：结构化输出的成功率高度依赖于底层 LLM（如 Qwen-Plus/GPT-4 等）对 <strong>JSON Schema</strong> 和 <strong>函数调用（Function Calling）</strong> 的支持及理解能力。对于较弱的模型，结果可能不准确或返回非 JSON 文本。</li><li><strong>提示词质量</strong>：用户提示词（<code>stringTemplate</code>）应包含足够且明确的信息，以便模型能够准确地将信息提取并映射到 <code>StudentRecord</code> 的字段上。</li></ul><h2 id="--">五、文本向量化 存储 检索</h2><p>文本向量化（也称为文本嵌入，Embedding）是将人类可读的文本信息转换为计算机可以理解和处理的数值形式（即高维向量）的过程。
文本向量化将文本的含义（语义）映射到向量空间中的位置。在向量空间中，语义相似的词语或句子（例如“狗”和“宠物”）会彼此靠近，而语义不相关的会彼此远离。
传统的全文检索（如关键词匹配）无法理解“意思”。通过向量化，我们可以计算用户问题向量与知识库中文本向量的距离（相似度），从而实现基于含义的检索，这是 RAG 机制的基石。
将文本转换为向量后，计算机可以使用成熟的线性代数和距离算法（如余弦相似度）进行快速、大规模的量化分析和搜索，极大地提高了处理效率。</p><h3 id="51-">5.1 前提条件</h3><ol start="1"><li>准备向量数据库实例：
<ul><li>你需要准备至少一种向量数据库
您需要准备一个正在运行的向量数据库实例，例如：</li><li><strong>PostgreSQL + PgVector 扩展</strong></li><li><strong>Redis + RediSearch 模块</strong></li><li><strong>Chroma、Milvus</strong> 等专用向量数据库。</li><li>安装要求： 请确保您已按照先前文档或官方指南完成了所选数据库的安装和配置。</li></ul></li><li>准备 <strong>Embedding</strong> 模型 (Embedding Model)：
<ul><li>向量存储要求将文本数据转换为数值向量（即嵌入）。因此，您必须配置一个可用的 Embedding 模型：
<ul><li><strong>Ollama Embedding Model：</strong> 适合本地开发和测试。</li><li><strong>DashScope Embedding Model：</strong> 阿里云通义千问的 Embedding 服务。</li><li><strong>OpenAI、Mistral</strong> 等其他供应商的 Embedding 模型。</li></ul></li></ul></li></ol><ul><li><strong>配置要求：</strong> 确保您的 <code>application.properties</code>/<code>application.yml</code> 中已配置相应的 API Key 或 Base URL。</li></ul><h3 id="52-">5.2 相关依赖</h3><pre class="language-xml lang-xml"><code class="language-xml lang-xml">&lt;!-- 添加 Redis (RedisStack) 向量数据库依赖 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
    &lt;artifactId&gt;spring-ai-starter-vector-store-redis&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;!-- 添加 postgres 向量数据库依赖 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
    &lt;artifactId&gt;spring-ai-starter-vector-store-pgvector&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;!-- ollama --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
    &lt;artifactId&gt;spring-ai-starter-model-ollama&lt;/artifactId&gt;
    &lt;version&gt;1.0.0&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<h3 id="53-">5.3 相关配置</h3><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml">spring:
  application:
    name: vector
  # pgvector (不是普通的postgres)
  datasource:
    url: jdbc:postgresql://127.0.0.1:5432/postgres
    username: postgres
    password: postgres
    driver-class-name: org.postgresql.Driver
  data:
    # redis stack (不是普通的redis)
    redis:
      host: localhost
      port: 6380
      database: 0
      connect-timeout: 3
      timeout: 3
  ai:
    dashscope:
      api-key: ${DASH_SCOPE_API_KEY}
      chat:
        options:
          model: qwen-plus-2025-09-11
      # embedding模型
      embedding:
        options:
          model: text-embedding-v4
    ollama:
      base-url: http://192.168.187.166:11434
      # embedding模型
      embedding:
        options:
          model: bge-m3:latest
    # 向量数据库配置 
    vectorstore:
      redis:
        index-name: custom-index
        prefix: custom-prefix
        initialize-schema: true
      pgvector:
        initialize-schema: true
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1024
        max-document-batch-size: 10000
</code></pre>
<h3 id="54--vector-store-">5.4 解决多 Vector Store 冲突的方案</h3><p>Spring AI 为每个支持的向量数据库提供了 <strong>自动配置（AutoConfiguration）</strong> 类。在配置了相应的连接信息后，这些自动配置类会尝试创建一个名为 <strong><code>vectorStore</code></strong> 的 Bean。</p><p>如果您的项目中同时引入了多个向量数据库的依赖（例如，同时引入了 Redis 和 PgVector 的依赖），Spring Boot 将尝试执行以下操作：</p><ol start="1"><li><p><strong>PgVector 自动配置</strong> 尝试注册一个名为 <strong><code>vectorStore</code></strong> 的 Bean。</p></li><li><p><strong>Redis 自动配置</strong> 随后也尝试注册一个名为 <strong><code>vectorStore</code></strong> 的 Bean。</p></li></ol><p>由于 Spring Boot 默认禁止覆盖已注册的 Bean 定义，这会导致底层的 <strong>Bean 名称注册冲突</strong>，应用程序会抛出错误并<strong>启动失败</strong>：</p><blockquote><p><strong>错误信息示例:</strong> The bean &#x27;vectorStore&#x27; ... could not be registered. A bean with that name has already been defined... and overriding is disabled.</p></blockquote>
<p><strong>Bean 定义名称冲突</strong>会直接阻止 Spring 容器完成初始化。</p><p> <strong>解决方案：手动排除自动配置并创建 Bean</strong></p><p>为了避免这种底层的名称冲突，最健壮的方法是阻止 Spring Boot 自动加载所有冲突的 <code>VectorStore</code> 自动配置类，然后手动创建您需要的那个 Bean。</p><ol start="1"><li><strong>禁用自动配置：</strong> 在主启动类上使用 <code>@SpringBootApplication(exclude = ...)</code> 注解，明确禁用冲突的 <code>AutoConfiguration</code> 类。</li></ol><pre class="language-java lang-java"><code class="language-java lang-java">@SpringBootApplication(exclude = {
        // 排除 Redis 的自动配置
        RedisVectorStoreAutoConfiguration.class,
        // 排除 PgVector 的自动配置
        PgVectorStoreAutoConfiguration.class
})
public class VectorApplication {
    // ...
}
</code></pre>
<ol start="2"><li><p><strong>手动创建 Bean：</strong> 在您的自定义配置类中，手动创建并注册您想使用的 <code>VectorStore</code> 实例（例如 PgVectorStore）。</p><p>因为引入了阿里的DashScope有多个 <code>EmbeddingModel</code> ，需要手动指定一些用哪个 <code>EmbeddingModel</code></p><pre class="language-java lang-java"><code class="language-java lang-java">@Configuration
public class VectorStoreConfig {
 @Resource
 @Qualifier(&quot;ollamaEmbeddingModel&quot;)
 private EmbeddingModel ollamaEmbeddingModel;
 @Resource
 private JedisPooled jedisClient;
 @Bean(&quot;redisVectorStore&quot;)
 public VectorStore redisVectorStore() {
     return RedisVectorStore.builder(jedisClient, ollamaEmbeddingModel)
             .indexName(&quot;custom-index&quot;)
             .prefix(&quot;custom-prefix:&quot;)
             .initializeSchema(true)
             .vectorAlgorithm(HSNW)
             .build();
 }
 @Bean(&quot;pgVectorStore&quot;)
 public VectorStore pgVectorStore(JdbcTemplate jdbcTemplate) {
     return PgVectorStore.builder(jdbcTemplate, ollamaEmbeddingModel)
             // 如果不指定默认是public
             .schemaName(&quot;embedding_vector&quot;)
             .vectorTableName(&quot;vector_store&quot;)
             .dimensions(1024)
             .maxDocumentBatchSize(1000)
             .build();
 }
}
</code></pre><h3 id="55-">5.5 文本向量化实现</h3><p>文本向量化的实现原理是通过深度学习模型（即 Embedding Model）将文本输入映射到高维空间中的一个密集数值向量，确保语义相似的文本在向量空间中彼此靠近。
<strong>示例代码：</strong></p><pre class="language-java lang-java"><code class="language-java lang-java">@RestController
@RequestMapping(&quot;/vector&quot;)
public class VectorController {
 @Resource
 @Qualifier(&quot;ollamaEmbeddingModel&quot;)
 private EmbeddingModel ollamaEmbeddingModel;
 @GetMapping(&quot;/text2embed&quot;)
 public EmbeddingResponse text2Embed(@RequestParam(name = &quot;text&quot;, defaultValue = &quot;你好&quot;) String text) {
     EmbeddingResponse embeddingResponse = ollamaEmbeddingModel.call(new EmbeddingRequest(List.of(text),
             DashScopeEmbeddingOptions.builder().withModel(&quot;bge-m3:latest&quot;).build()));
     System.out.println(Arrays.toString(embeddingResponse.getResult().getOutput()));
     return embeddingResponse;
 }
}
</code></pre>
</li></ol><h3 id="56-">5.6 向量存储</h3><p>为什么需要向量存储？向量存储是为了高效地存储和检索大量的高维文本向量，它是实现 RAG（检索增强生成）机制中语义检索功能的核心基础设施。</p><p>如果你是 <code>pgvector</code>，需要如下检查。需要检查你创建的 <code>vector</code> 类型和 <code>embedding</code> 模型所使用的向量维度是否一致。</p><pre class="language-sql lang-sql"><code class="language-sql lang-sql">--查看是否有 &#x27;vector&#x27; 拓展
SELECT name, default_version, installed_version
FROM pg_available_extensions
WHERE name = &#x27;vector&#x27;;
-- 当前数据库中创建 vector 拓展。vector 扩展（即 PgVector）提供了 vector 数据类型和相关的索引（如 HNSW、IVFFlat）操作符，是 PostgreSQL 能够存储和高效检索向量数据的核心。
CREATE
EXTENSION IF NOT EXISTS vector;
-- 创建 hstore 扩展。 hstore 扩展提供了用于存储键值对数据的 hstore 数据类型。在 PgVector 存储中，它常被用来存储向量关联的 Metadata（元数据），例如文档来源、作者、时间戳等信息。
CREATE
EXTENSION IF NOT EXISTS hstore;
-- 创建 uuid-ossp 扩展。 该扩展提供了生成 UUID (Universally Unique Identifier) 的函数，例如 uuid_generate_v4()。这在创建表时用作主键，保证每条记录 ID 的唯一性。
CREATE
EXTENSION IF NOT EXISTS &quot;uuid-ossp&quot;;
-- 查看名为 &#x27;vector&#x27; 的扩展是否已经被安装到当前数据库中
SELECT extname, extversion, extnamespace::regnamespace AS schema
FROM pg_extension
WHERE extname = &#x27;vector&#x27;;
-- 创建一个名为 vector_store 的表，用于存储您的向量数据。
CREATE TABLE IF NOT EXISTS vector_store
(
    id        uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
    content   text,
    metadata  json,
    embedding vector(1536)
    );
-- 修改 vector_store 表中 embedding 列的定义。将向量列的期望维度从 1536 修改为 1024。这条语句是用来解决您上一个错误 &quot;expected 1536 dimensions, not 1024&quot; 的核心操作。它确保数据库的表结构与您的 Embedding Model (Ollama/DashScope) 实际生成的 1024 维向量相匹配。
ALTER TABLE vector_store
ALTER COLUMN embedding TYPE vector(1024);
</code></pre><p><strong>示例代码：</strong></p><pre class="language-java lang-java"><code class="language-java lang-java">public void add() {
    List&lt;Document&gt; documents = List.of(
            new Document(&quot;咖啡豆的最佳烘焙温度通常在 200°C 左右。&quot;),
            new Document(&quot;今天的会议将在下午三点开始，主要讨论季度预算。&quot;),
            new Document(&quot;大型语言模型（LLM）在自然语言处理领域取得了突破性进展。&quot;),
            new Document(&quot;我最喜欢在清晨的湖边散步，那里的空气十分清新。&quot;),
            new Document(&quot;红烧肉的秘诀在于小火慢炖和适量的冰糖。&quot;),
            new Document(&quot;向量数据库是存储和检索高维向量数据的关键技术。&quot;),
            new Document(&quot;如何通过梯度下降法优化神经网络的权重？&quot;),
            new Document(&quot;制作美味拿铁的关键是牛奶的完美发泡。&quot;),
            new Document(&quot;机器学习和深度学习是人工智能的两个主要分支。&quot;),
            new Document(&quot;本年度的财务报告将于本周五前提交给董事会审阅。&quot;));
    // 使用redis stack存储
    redisVectorStore.add(documents);
    // 使用pgvector 存储
    pgVectorStore.add(documents);
}
</code></pre>
<h3 id="57-">5.7 相似度检索</h3><p><strong>相似度检索</strong>的原理是计算用户查询文本（已向量化）与向量存储中所有文档向量之间的<strong>距离或夹角</strong>（如余弦相似度），以找出向量空间中<strong>距离最近</strong>（即语义最相关）的 $k$ 个文档片段。</p><p><strong>使用示例：</strong></p><p>该示例展示了如何使用 SearchRequest 向配置好的 Redis 和 PgVector 向量存储发起检索请求，并获取最相似的 Top K 文档。</p><pre class="language-java lang-java"><code class="language-java lang-java">public Map&lt;String, List&lt;Document&gt;&gt; getAll(@RequestParam(name = &quot;text&quot;, defaultValue = &quot;AI 技术的核心发展方向是什么？&quot;) String text) {
    SearchRequest searchRequest = SearchRequest.builder().query(text).topK(2).build();
    redisVectorStore.similaritySearch(searchRequest);
    pgVectorStore.similaritySearch(searchRequest);
    HashMap&lt;String, List&lt;Document&gt;&gt; map = new HashMap&lt;&gt;();
    map.put(&quot;redisVectorStore&quot;, redisVectorStore.similaritySearch(searchRequest));
    map.put(&quot;pgVectorStore&quot;, pgVectorStore.similaritySearch(searchRequest));
    return map;
}
</code></pre><h2 id="rag">六、RAG增加检索</h2><p>上面我们已经实现了<strong>向量存储</strong>和<strong>向量检索</strong>的基础，下面我们将核心实现<strong>RAG（Retrieval-Augmented Generation）</strong>，即给大模型外挂一个自定义“知识库”。</p><p>RAG的流程是：用户提出问题后，系统先从知识库中检索出<strong>最相关</strong>的内容片段（即<strong>Context</strong>），然后将这些内容和用户的问题一起喂给大模型（LLM），引导大模型基于这些上下文来生成回答。</p><p>依赖和配置与之前相同，此处不再赘述。</p><h3 id="61-">6.1 知识库内容示例</h3><p>我们将使用以下自定义运维错误码作为我们的知识库：
<code>classpath:/knowledge-base/ops.txt</code>位置的文本内容如下</p><pre class="language-text lang-text"><code class="language-text lang-text">00000 系统OK正确执行后的返回
A0001 用户端错误一级宏观错误码
A0100 用户注册错误二级宏观错误码
B1111 支付接口超时
C2222 Kafka消息解压严重
</code></pre><p>正常情况下，大模型并不知道我们自定义的这些错误码的含义。通过 RAG 机制，我们可以确保大模型在回答相关问题时能够查阅并引用这些专业知识。</p><h3 id="62-">6.2 知识库加载与向量存储初始化</h3><p>这一步骤负责将本地的知识文本（<code>ops.txt</code>）读取、分割、并将其嵌入（Embedding）后存入向量数据库（这里是 PostgreSQL 和 Redis），以便后续进行语义检索。</p><blockquote><p><strong>核心组件：</strong></p><ul><li><p><code>TextReader</code>：读取文件内容。</p></li><li><p><code>TokenTextSplitter</code>：将大块文本分割成适合嵌入的小块 <code>Document</code>。</p></li><li><p><code>VectorStore</code>：存储嵌入后的向量。</p></li><li><p><strong>防重复加载机制：</strong> 利用 Redis 的 <code>setIfAbsent</code> 确保在服务重启时不会重复加载相同的知识库。</p></li></ul></blockquote>
<pre class="language-java lang-java"><code class="language-java lang-java">@Configuration
public class InitVectorDatabaseConfig {
    @Resource
    @Qualifier(&quot;pgVectorStore&quot;)
    private VectorStore pgVectorStore;
    @Resource
    @Qualifier(&quot;redisVectorStore&quot;)
    private VectorStore redisVectorStore;
    private final RedisTemplate&lt;String, String&gt; redisTemplate;
    public InitVectorDatabaseConfig(RedisTemplate&lt;String, String&gt; redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    // 这里是外挂知识库的位置
    @Value(&quot;classpath:/knowledge-base/ops.txt&quot;)
    private org.springframework.core.io.Resource opsFile;

    @PostConstruct
    public void init() {
        TextReader textReader = new TextReader(opsFile);
        textReader.setCharset(Charset.defaultCharset());
        List&lt;Document&gt; list = new TokenTextSplitter().transform(textReader.read());
        String sourceMetadata = (String) textReader.getCustomMetadata().get(&quot;source&quot;);
        String textHash = SecureUtil.md5(sourceMetadata);
        // 为 PostgreSQL 和 Redis 分别设置防重复键
        String pgRedisKey = &quot;vector-pg:&quot; + textHash;
        String redisRedisKey = &quot;vector-redis:&quot; + textHash;
        // PostgreSQL 防重复检查
        Boolean pgFlag = redisTemplate.opsForValue().setIfAbsent(pgRedisKey, &quot;1&quot;);
        if (Boolean.TRUE.equals(pgFlag)) {
            pgVectorStore.add(list);
            System.out.println(&quot;PostgreSQL 向量数据初始化完成&quot;);
        } else {
            System.out.println(&quot;PostgreSQL 向量数据已存在，跳过初始化&quot;);
        }
        // Redis 防重复检查
        Boolean redisFlag = redisTemplate.opsForValue().setIfAbsent(redisRedisKey, &quot;1&quot;);
        if (Boolean.TRUE.equals(redisFlag)) {
            redisVectorStore.add(list);
            System.out.println(&quot;Redis 向量数据初始化完成&quot;);
        } else {
            System.out.println(&quot;Redis 向量数据已存在，跳过初始化&quot;);
        }
    }
}
</code></pre><h3 id="63--rag-">6.3 实现 RAG 检索增强</h3><p>我们使用 Spring AI 提供的 <code>RetrievalAugmentationAdvisor</code> 来实现 RAG 机制。这个 Advisor 会在发送请求给大模型<strong>之前</strong>自动执行检索，并将检索结果作为上下文（Context）注入到最终的 Prompt 中。</p><pre class="language-java lang-java"><code class="language-java lang-java">@Resource
@Qualifier(&quot;pgVectorStore&quot;)
private VectorStore pgVectorStore;
@Resource
@Qualifier(&quot;redisVectorStore&quot;)
private VectorStore redisVectorStore;
public Flux&lt;String&gt; redisRag(String code) {
    // 1. 定义系统角色和指令
    String systemInfo = &quot;&quot;&quot;
            你是一个专业的运维工程师,你的任务是根据提供的编码给出对应故障解释。
            你必须严格根据检索到的知识库内容进行回答,如果知识库中找不到匹配的信息,则回复“抱歉,知识库中未找到该故障码的解释。”
            &quot;&quot;&quot;;
    // 2. 配置检索增强 Advisor
    RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
            // 绑定知识库检索器：这里指定使用 RedisVectorStore 进行检索
            .documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(redisVectorStore).build())
            // 绑定知识库检索器：这里指定使用 PostgreSQLVectorStore 进行检索
            //.documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(pgVectorStore).build())
            .build();
    // 3. 构建并发送请求
    return chatClient
            .prompt()
            .system(systemInfo) // 设置系统角色
            .user(code)        // 用户提问 (即查询的错误码)
            .advisors(advisor) // 关键：注入 RAG 检索增强 Advisor
            .stream()
            .content();
}
</code></pre>
<p>当用户调用 <code>redisRag(&quot;A0100&quot;)</code> 时：</p><ol start="1"><li><p><code>RetrievalAugmentationAdvisor</code> 会根据用户输入的 <code>&quot;A0100&quot;</code> 到 <code>redisVectorStore</code> 中进行<strong>语义检索</strong>。</p></li><li><p>检索结果（例如：<code>A0100 用户注册错误二级宏观错误码</code>）被提取出来，作为 <strong>Context</strong>。</p></li><li><p>Advisor 将 Context、<code>systemInfo</code> 和用户问题 <code>&quot;A0100&quot;</code> 组合成一个<strong>最终的 Prompt</strong> 发送给大模型。</p></li><li><p>大模型根据这个增强后的 Prompt，结合 Context 给出准确的回答。</p></li></ol><h2 id="">七、工具调用</h2><p><strong>工具调用</strong>机制允许大语言模型（LLM）在理解用户的意图后，识别并调用外部定义的函数或服务，从而获取实时信息、执行特定操作或访问外部系统。它极大地扩展了 LLM 的能力边界。</p><h3 id="71-">7.1 为什么需要工具调用？</h3><p>大模型的知识通常截止于其训练数据的截止日期，它无法：</p><ol start="1"><li><strong>获取实时信息：</strong> 例如，当前时间、天气、实时股价等。</li><li><strong>执行外部操作：</strong> 例如，发送邮件、调用支付接口、查询数据库等。</li></ol><p>工具调用机制通过以下方式解决了这些限制：</p><table><thead><tr><th><strong>机制</strong></th><th><strong>描述</strong></th><th><strong>示例</strong></th></tr></thead><tbody><tr><td><strong>功能描述暴露</strong></td><td>开发者将外部函数（工具）的名称、用途和参数通过 JSON Schema 格式<strong>描述</strong>给 LLM。</td><td>告诉 LLM 有一个名为 <code>getCurrentTime</code> 的函数，用来“获取当前时间”。</td></tr><tr><td><strong>意图推理</strong></td><td>LLM 根据用户输入，<strong>推理</strong>出用户是否需要调用某个工具，并决定调用哪个工具及其所需参数。</td><td>用户问“现在几点？”，LLM 推理出需要调用 <code>getCurrentTime()</code>。</td></tr><tr><td><strong>执行与生成</strong></td><td>应用接收到 LLM 的调用指令后，<strong>执行</strong>该工具，将工具的<strong>结果</strong>作为新的上下文返回给 LLM，LLM 再基于结果生成最终回复。</td><td>应用执行 <code>getCurrentTime()</code> 返回 <code>2025-12-02T17:28:23.123</code>，LLM 回复：“现在是 2025 年 12 月 2 日下午 5 点 28 分。”</td></tr></tbody></table>
<h3 id="72-">7.2 定义工具类</h3><p>在 Spring AI 中，您只需要在 Java 方法上使用<code> @Tool</code> 注解，即可将其暴露为 LLM 可以理解的工具。</p><pre class="language-java lang-java"><code class="language-java lang-java">public class DateTimeTools {
    /**
     * 获取当前系统时间。
     * @Tool 注解将此方法注册为可供大模型调用的工具。
     * returnDirect = false: 表示工具结果将返回给大模型进行总结和生成。
     */
    @Tool(description = &quot;获取当前时间&quot;, returnDirect = false)
    public String getCurrentTime() {
        return LocalDateTime.now().toString();
    }
}
</code></pre><h3 id="73-">7.3 调用工具</h3><p>Spring AI 提供了两种主要方式来集成和调用工具：基于低级 <code>ChatModel</code> 和基于高级 <code>ChatClient</code>。</p><h4 id="731--chatmodel-">7.3.1 基于 <code>ChatModel</code> 实现的工具调用</h4><p>这种方式需要手动构造 <code>ToolCallbacks</code> 数组，并将其注入到 <code>ChatOptions</code> 中，然后传递给 <code>ChatModel</code>。</p><pre class="language-java lang-java"><code class="language-java lang-java">@RequestMapping(&quot;/tool&quot;)
public class ToolCallingController {
    @Resource
    private ChatModel qwenChatModel;
    @GetMapping(&quot;/model&quot;)
    public Flux&lt;String&gt; chat(@RequestParam(name = &quot;question&quot;, defaultValue = &quot;你是谁现在几点?&quot;) String question) {
        // 1. 工具注册到工具集合里（ToolCallbacks负责将Java对象转换为LLM可读的工具描述）
        ToolCallback[] tools = ToolCallbacks.from(new DateTimeTools());
        // 2. 将工具集配置进ChatOptions对象，启用工具调用模式
        ChatOptions options = ToolCallingChatOptions.builder().toolCallbacks(tools).build();
        // 3. 构建提示词，携带工具配置
        Prompt prompt = new Prompt(question, options);
        // 4. 调用大模型并流式处理结果
        // ChatModel 会自动处理工具调用循环：LLM返回调用指令 -&gt; 应用执行工具 -&gt; 应用将结果返回给LLM
        return qwenChatModel.stream(prompt).mapNotNull(
                chatResponse -&gt; chatResponse.getResult().getOutput().getText()
        );
    }
    // ...
}
</code></pre><h4 id="732--chatclient-">7.3.2 基于 <code>ChatClient</code> 实现的工具调用</h4><p><code>ChatClient</code> 提供了更简洁的流式 API，它在底层自动封装了工具注册和配置的细节。</p><pre class="language-java lang-java"><code class="language-java lang-java">// ...
@Resource
private ChatClient qwenChatClient;
// ...
@GetMapping(&quot;/client&quot;)
public Flux&lt;String&gt; chat2(@RequestParam(name = &quot;question&quot;, defaultValue = &quot;你是谁现在几点?&quot;) String question) {
    return qwenChatClient.prompt(question)
            // 通过 tools() 方法直接传入工具实例
            .tools(new DateTimeTools()) 
            .stream()
            .content(); // 流式获取最终回答内容
}
</code></pre><h2 id="mcp">八、MCP</h2><p><strong>MCP (Model-Powered Control Plane)</strong> 是一种架构，利用 AI Agent 的推理能力，将<strong>自然语言指令转化为对复杂系统的编排和管理</strong>。<strong>工具调用 (Tool Calling)</strong> 则是实现 MCP 的<strong>底层机制</strong>，它为 LLM 提供了调用外部函数的能力；简单来说，工具调用是 LLM 的“手脚”，而 MCP 是利用这些“手脚”去管理复杂系统的 “大脑”和框架。</p><h3 id="81-mcp-">8.1 MCP 服务端</h3><h4 id="811">8.1.1依赖</h4><pre class="language-xml lang-xml"><code class="language-xml lang-xml">&lt;dependencies&gt;
    &lt;!--注意事项
        spring-ai-starter-mcp-server-webflux不能和&lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;依赖并存，
        否则会使用tomcat启动,而不是netty启动，从而导致mcpserver启动失败，但程序运行是正常的，mcp客户端连接不上。
    --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!--mcp-server-webflux--&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
        &lt;artifactId&gt;spring-ai-starter-mcp-server-webflux&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!--lombok--&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
        &lt;artifactId&gt;lombok&lt;/artifactId&gt;
        &lt;version&gt;1.18.38&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;!--hutool--&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;cn.hutool&lt;/groupId&gt;
        &lt;artifactId&gt;hutool-all&lt;/artifactId&gt;
        &lt;version&gt;5.8.22&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;
</code></pre><h4 id="812-">8.1.2 配置文件</h4><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml">spring:
  application:
    name: mcp-server
  ai:
    mcp:
      server:
        type: async
        name: mcp-server
        version: 1.0.0
</code></pre><h4 id="813-">8.1.3 工具定义与暴露</h4><p>在模型驱动控制平面（MCP）架构中，<strong>服务端</strong>负责定义并暴露一系列具体的业务功能或操作（即<strong>工具</strong>）。这些工具将被 AI Agent 或 MCP 客户端调用，以执行复杂任务。</p><h5 id="8131-">8.1.3.1 编写服务类并暴露工具方法</h5><p>我们通过在服务类方法上使用 Spring AI 的 <code>@Tool</code> 注解，将方法标记为可供大模型调用的工具。</p><h5 id="8132-">8.1.3.2 示例：城市旅游新闻服务</h5><p>以下是一个模拟的在线旅游新闻服务，它接受城市名称作为参数，并返回该城市的今日旅游新闻摘要。</p><pre class="language-java lang-java"><code class="language-java lang-java">/**
 * 城市旅游新闻服务：为AI Agent提供今日城市旅游头条信息。
 */
@Service
public class TourismNewsService {
    /**
     * 根据城市名称获取今日热门旅游头条新闻或活动信息。
     * * @param city 城市名称，如“北京”、“上海”、“深圳”
     *
     * @return 城市今日旅游头条新闻摘要
     */
    @Tool(description = &quot;根据城市名称获取今日热门旅游头条新闻或活动信息&quot;)
    public String getCityTourismNews(String city) {
        // 使用 Map 模拟数据库或外部 API 返回的今日头条数据
        Map&lt;String, String&gt; newsMap = Map.of(
                &quot;北京&quot;, &quot;今日头条：故宫博物院启动秋季“清代宫廷生活艺术”特展，限量门票现已开放线上预约。&quot;,
                &quot;上海&quot;, &quot;今日头条：外滩举办国际艺术周，多家知名美术馆延长开放时间，黄浦江游船票预订火爆。&quot;,
                &quot;深圳&quot;, &quot;今日头条：欢乐谷主题公园推出五折特惠活动，庆祝深圳特区成立周年，吸引大量家庭出游。&quot;
        );
        return newsMap.getOrDefault(city, &quot;抱歉：未查询到该城市今日旅游热门新闻！&quot;);
    }
}
</code></pre><h4 id="814--mcp-">8.1.4 将工具注册到 MCP 框架</h4><p>为了让 MCP 客户端（即调用大模型的应用）能够访问和使用这些工具，我们需要通过 Spring AI 的配置机制，将这些服务实例注册为一个<strong>工具回调提供者</strong>（<code>ToolCallbackProvider</code>）。</p><blockquote><p><strong>核心组件：</strong></p><ul><li><p><strong><code>ToolCallbackProvider</code>：</strong> 这是 Spring AI 中用于集中管理和暴露所有工具定义的接口。</p></li><li><p><strong><code>MethodToolCallbackProvider</code>：</strong> 默认实现，它通过反射机制扫描传入的 Java 对象（<code>toolObjects</code>）中所有带有 <code>@Tool</code> 注解的方法，并将它们转换成大模型能够理解的 <strong>JSON Schema</strong> 格式。</p><pre class="language-java lang-java"><code class="language-java lang-java">@Configuration
public class McpServerConfig {
  public final WeatherService weatherService;
  public final TourismNewsService tourismNewsService;
  public McpServerConfig(WeatherService weatherService,
                         TourismNewsService tourismNewsService) {
      this.weatherService = weatherService;
      this.tourismNewsService = tourismNewsService;
  }
  /**
   * 将工具方法暴露给外部 mcp client 调用
   *
   * @return ToolCallbackProvider
   */
  @Bean
  public ToolCallbackProvider weatherTools() {
      return MethodToolCallbackProvider.builder()
              .toolObjects(weatherService, tourismNewsService)
              // .toolObjects(tourismNewsService)
              .build();
  }
}
</code></pre>
</li></ul></blockquote>
<h3 id="82-mcp-">8.2 MCP 客户端</h3><p>MCP 客户端是 AI Agent 侧的应用程序，它负责连接到 MCP 服务端，获取工具的定义（JSON Schema），并在需要时发起对远程工具的实际调用。</p><h4 id="821-">8.2.1 客户端依赖</h4><pre class="language-xml lang-xml"><code class="language-xml lang-xml">  &lt;!-- 2.mcp-clent 依赖 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
    &lt;artifactId&gt;spring-ai-starter-mcp-client&lt;/artifactId&gt;
&lt;/dependency&gt;
</code></pre><h4 id="822-">8.2.2 客户端配置文件</h4><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml">spring:
  application:
    name: mcp-client
  ai:
    dashscope:
      api-key: ${DASH_SCOPE_API_KEY}
      base-url: &quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;
      chat:
        options:
          model: &quot;qwen-plus-2025-09-11&quot;
    mcp:
      client:
        type: async
        name: mcp-client
        version: 1.0.0
        request-timeout: 60s
        toolcallback:
          enabled: true
        sse:
          connections:
            # 你的服务端地址
            mcp-server1:
              url: http://localhost:801
</code></pre><h4 id="823--chatclient">8.2.3 客户端配置：集成远程工具到 ChatClient</h4><p>为了让 <code>ChatClient</code> 在每次调用时都知晓并携带可用的工具定义，我们需要将 MCP 客户端获取到的远程工具注入为 <code>ChatClient</code> 的默认工具回调。</p><pre class="language-java lang-java"><code class="language-java lang-java">@Configuration
public class SaaLLMConfig {
    @Bean
    public ChatClient chatClient(ChatModel chatModel, ToolCallbackProvider tools) {
        return ChatClient.builder(chatModel)
                .defaultToolCallbacks(tools.getToolCallbacks())
                .build();
    }
}
</code></pre>
<h4 id="824-mcp">8.2.4 调用MCP服务</h4><p>一旦 ChatClient 被配置了 MCP 提供的远程工具，客户端的调用代码就变得极其简洁。开发者无需关心工具在本地还是远程，只需像调用普通 LLM 一样发起请求。</p><pre class="language-java lang-java"><code class="language-java lang-java">@Resource
private ChatClient chatClient;
public Flux&lt;String&gt; chat(@RequestParam(name = &quot;question&quot;, defaultValue = &quot;北京&quot;) String question) {
    return chatClient.prompt(question).stream().content();
}
</code></pre>
<h3 id="83-mcppostgres-mcp">8.3 调用其他的MCP服务(postgres mcp)</h3><p>除了连接远程 HTTP/SSE 服务外，Spring AI 的 MCP 客户端还支持通过<strong>标准输入/输出（<code>stdio</code>）</strong>方式，启动并连接本地进程运行的 MCP 服务（如用于数据库查询的 MCP）。
这将赋予大模型直接<strong>查询本地数据库结构和数据</strong>的能力，无需编写传统的 SQL 代码。</p><p>依赖用上面mcp 客户端依赖</p><h4 id="831--json--classpathmcpjson">8.3.1 准备服务 JSON 文件 (classpath:/mcp.json)</h4><p>我们使用一个 JSON 文件来定义需要通过本地进程启动和管理的 MCP 服务。这里我们配置一个名为 <code>postgres</code> 的服务。</p><blockquote><p><strong>注意：</strong> 这里的配置是指示 Spring AI 客户端如何通过 Node.js (<code>npx</code>) 启动并连接 PostgreSQL MCP 服务。你需要确保本地环境有 Node.js 和 <code>npx</code> 可用。</p></blockquote>
<p>这里可以添加多个mcp，可访问<a href="https://mcp.so/">mcp server</a>获取更多mcp服务。</p><pre class="language-json5 lang-json5"><code class="language-json5 lang-json5">{
  &quot;mcpServers&quot;: {
    &quot;postgres&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-postgres&quot;,
        &quot;postgresql://postgres:postgres@127.0.0.1:5432/postgres&quot;
      ],
      &quot;env&quot;: {
        &quot;PATH&quot;: &quot;/home/ice/.nvm/versions/node/v22.21.1/bin:/usr/local/bin:/usr/bin:/bin&quot;
      }
    }
  }
}
</code></pre><h4 id="832-">8.3.2 配置文件</h4><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml">spring:
  application:
    name: mcp-client
  ai:
    dashscope:
      api-key: ${DASH_SCOPE_API_KEY}
      chat:
        options:
          model: &quot;qwen-plus-2025-09-11&quot;
    mcp:
      client:
        request-timeout: 60s
        toolcallback:
          enabled: true
        stdio:
          # 刚刚的json 文件位置
          servers-configuration: classpath:/mcp.json
        enabled: true
        root-change-notification: true
</code></pre><h4 id="833--chatclient">8.3.3 注册到 ChatClient</h4><p>与集成远程 HTTP/SSE 服务一样，我们需要将 MCP 客户端获取到的所有工具（包括 PostgreSQL 查询工具）注册给 <code>ChatClient</code>。</p><pre class="language-java lang-java"><code class="language-java lang-java">@Configuration
public class SaaLLMConfig {
    @Bean
    public ChatClient chatClient(ChatModel chatModel, ToolCallbackProvider tools) {
        return ChatClient.builder(chatModel)
                //mcp协议，配置见yml文件，此处只赋能给ChatClient对象
                .defaultToolCallbacks(tools.getToolCallbacks())
                .build();
    }
}
</code></pre><h4 id="834--mcp-">8.3.4 调用 MCP 服务（数据库查询）</h4><p>一旦配置完成，大模型就获得了查询数据库的能力。您可以向它提出关于数据库结构的自然语言问题，它将通过调用 PostgreSQL MCP 工具来获取数据并生成回答。</p><pre class="language-java lang-java"><code class="language-java lang-java">public Flux&lt;String&gt; chat(String question) {
    return chatClient.prompt(question).stream().content();
}
</code></pre></div><p style="text-align:right"><a href="https://dryice.icu/posts/technology/spring-ai-study#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://dryice.icu/posts/technology/spring-ai-study</link><guid isPermaLink="true">https://dryice.icu/posts/technology/spring-ai-study</guid><dc:creator><![CDATA[ice]]></dc:creator><pubDate>Mon, 08 Dec 2025 13:21:29 GMT</pubDate></item><item><title><![CDATA[使用rclone服务器外挂Google Drive]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120155813856_repeat_1763625496676__432829.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120160105704_repeat_1763625667279__355609.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120160611663_repeat_1763625973188__766154.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120161510015_repeat_1763626511536__622555.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120161931482_repeat_1763626773048__561602.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120162202612_repeat_1763626924255__225517.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://dryice.icu/posts/technology/rclone-mount-gdrive">https://dryice.icu/posts/technology/rclone-mount-gdrive</a></blockquote><div><h2 id="">引子</h2><p>前不久入手了一台 <strong>16H16G 的服务器</strong>，111 三年的价格确实香。因为是小厂商的机器，多少还是有点“跑路风险”的担忧，所以我给一些关键数据做了额外备份。</p><p>这里我使用 <strong>rclone</strong> 将 <strong>Google Drive 挂载成服务器的一个磁盘目录</strong>，把需要长期保存的文件定时同步到这个“外挂硬盘”里，算是给数据多上一道保险。</p><p>为什么不直接把重要的数据盘放云盘挂载点？重要的数据都在数据库里，数据库的某些文件会处于一直读写的状态会频繁和Google Drive API 通信，会降低网站的访问速度。</p><p>目前 rclone 支持多达 63 种对象存储，包括微软、谷歌、阿里、移动、自建的 HDFS、iCloud 等主流服务，几乎覆盖了常见的云存储场景。</p><p>本文主要展示 Google Drive 的配置步骤。其他存储的接入方式基本类似，只是在授权流程上略有差异，整体操作思路一致。</p><p>下面是具体操作步骤。</p><h2 id="-rclone">安装 rclone。</h2><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># Arch Linux / Manjaro
sudo pacman -S rclone

# Debian/Ubuntu
sudo apt update 
sudo apt install rclone
</code></pre>
<p>验证安装是否成功</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">rclone version
</code></pre>
<p>应该显示类似信息：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">rclone version
rclone v1.71.2
- os/version: arch (64 bit)
- os/kernel: 6.17.7-arch1-1 (x86_64)
- os/type: linux
- os/arch: amd64
- go/version: go1.25.3 X:nodwarf5
- go/linking: dynamic
- go/tags: none
</code></pre>
<h2 id="">配置服务</h2><pre class="language-bash lang-bash"><code class="language-bash lang-bash">root@ser657914752113:/# rclone config
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
n/s/q&gt; n
</code></pre>
<p>输入n 创建新的远程</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">Enter name for new remote.
name&gt; gdrive
</code></pre><p>输入名称如我的叫 <code>gdrive</code> 回车</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.
 1 / 1Fichier
   \ (fichier)
 2 / Akamai NetStorage
   \ (netstorage)
 3 / Alias for an existing remote
   \ (alias)
 4 / Amazon S3 Compliant Storage Providers including AWS, Alibaba, ArvanCloud, Ceph, ChinaMobile, Cloudflare, DigitalOcean, Dreamhost, Exaba, FlashBlade, GCS, HuaweiOBS, IBMCOS, IDrive, IONOS, LyveCloud, Leviia, Liara, Linode, Magalu, Mega, Minio, Netease, Outscale, OVHcloud, Petabox, RackCorp, Rclone, Scaleway, SeaweedFS, Selectel, StackPath, Storj, Synology, TencentCOS, Wasabi, Qiniu, Zata and others
   \ (s3)
 5 / Backblaze B2
   \ (b2)
 6 / Better checksums for other remotes
   \ (hasher)
 7 / Box
   \ (box)
 8 / Cache a remote
   \ (cache)
 9 / Citrix Sharefile
   \ (sharefile)
10 / Cloudinary
   \ (cloudinary)
11 / Combine several remotes into one
   \ (combine)
12 / Compress a remote
   \ (compress)
13 / DOI datasets
   \ (doi)
14 / Dropbox
   \ (dropbox)
15 / Encrypt/Decrypt a remote
   \ (crypt)
16 / Enterprise File Fabric
   \ (filefabric)
17 / FTP
   \ (ftp)
18 / FileLu Cloud Storage
   \ (filelu)
19 / Files.com
   \ (filescom)
20 / Gofile
   \ (gofile)
21 / Google Cloud Storage (this is not Google Drive)
   \ (google cloud storage)
22 / Google Drive
   \ (drive)
23 / Google Photos
   \ (google photos)
24 / HTTP
   \ (http)
25 / Hadoop distributed file system
   \ (hdfs)
26 / HiDrive
   \ (hidrive)
27 / ImageKit.io
   \ (imagekit)
28 / In memory object storage system.
   \ (memory)
29 / Internet Archive
   \ (internetarchive)
30 / Jottacloud
   \ (jottacloud)
31 / Koofr, Digi Storage and other Koofr-compatible storage providers
   \ (koofr)
32 / Linkbox
   \ (linkbox)
33 / Local Disk
   \ (local)
34 / Mail.ru Cloud
   \ (mailru)
35 / Mega
   \ (mega)
36 / Microsoft Azure Blob Storage
   \ (azureblob)
37 / Microsoft Azure Files
   \ (azurefiles)
38 / Microsoft OneDrive
   \ (onedrive)
39 / OpenDrive
   \ (opendrive)
40 / OpenStack Swift (Rackspace Cloud Files, Blomp Cloud Storage, Memset Memstore, OVH)
   \ (swift)
41 / Oracle Cloud Infrastructure Object Storage
   \ (oracleobjectstorage)
42 / Pcloud
   \ (pcloud)
43 / PikPak
   \ (pikpak)
44 / Pixeldrain Filesystem
   \ (pixeldrain)
45 / Proton Drive
   \ (protondrive)
46 / Put.io
   \ (putio)
47 / QingCloud Object Storage
   \ (qingstor)
48 / Quatrix by Maytech
   \ (quatrix)
49 / SMB / CIFS
   \ (smb)
50 / SSH/SFTP
   \ (sftp)
51 / Sia Decentralized Cloud
   \ (sia)
52 / Storj Decentralized Cloud Storage
   \ (storj)
53 / Sugarsync
   \ (sugarsync)
54 / Transparently chunk/split large files
   \ (chunker)
55 / Uloz.to
   \ (ulozto)
56 / Union merges the contents of several upstream fs
   \ (union)
57 / Uptobox
   \ (uptobox)
58 / WebDAV
   \ (webdav)
59 / Yandex Disk
   \ (yandex)
60 / Zoho
   \ (zoho)
61 / iCloud Drive
   \ (iclouddrive)
62 / premiumize.me
   \ (premiumizeme)
63 / seafile
   \ (seafile)
Storage&gt; 22
</code></pre><p>输入22 选择 Google Drive</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">Option client_id.
Google Application Client Id
Setting your own is recommended.
See https://rclone.org/drive/#making-your-own-client-id for how to create your own.
If you leave this blank, it will use an internal key which is low performance.
Enter a value. Press Enter to leave empty.
client_id&gt; 
</code></pre><p>这里直接回车即可：
空着就行,你不追求高并发访问的话，用 rclone 内置的 client_id 完全够用。继续下一步。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">Option client_secret.
OAuth Client Secret.
Leave blank normally.
Enter a value. Press Enter to leave empty.
client_secret&gt; 
</code></pre><p>同样直接回车,留空即可。继续下一步。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">Option scope.
Comma separated list of scopes that rclone should use when requesting access from drive.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
 1 / Full access all files, excluding Application Data Folder.
   \ (drive)
 2 / Read-only access to file metadata and file contents.
   \ (drive.readonly)
   / Access to files created by rclone only.
 3 | These are visible in the drive website.
   | File authorization is revoked when the user deauthorizes the app.
   \ (drive.file)
   / Allows read and write access to the Application Data folder.
 4 | This is not visible in the drive website.
   \ (drive.appfolder)
   / Allows read-only access to file metadata but
 5 | does not allow any access to read or download file content.
   \ (drive.metadata.readonly)
scope&gt; 1
</code></pre><p>这里选 1，你要把 Google Drive 挂载到服务器当硬盘用，需要完整读写权限。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">Option service_account_file.
Service Account Credentials JSON file path.
Leave blank normally.
Needed only if you want use SA instead of interactive login.
Leading `~` will be expanded in the file name as will environment variables such as `${RCLONE_CONFIG_DIR}`.
Enter a value. Press Enter to leave empty.
service_account_file&gt; 
</code></pre><p>这里继续回车，留空即可：
你不需要 Service Account，这一步直接跳过。继续下一步。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">Edit advanced config?
y) Yes
n) No (default)
y/n&gt; n
</code></pre><p>这里选 n，不需要改高级配置。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">Use web browser to automatically authenticate rclone with remote?
 * Say Y if the machine running rclone has a web browser you can use
 * Say N if running rclone on a (remote) machine without web browser access
If not sure try Y. If Y failed, try N.

y) Yes (default)
n) No
y/n&gt; n
</code></pre><p>你这是在服务器里跑的，应该是没有浏览器的，选 n。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">Option config_token.
For this to work, you will need rclone available on a machine that has
a web browser available.
For more help and alternate methods see: https://rclone.org/remote_setup/
Execute the following on the machine with the web browser (same rclone
version recommended):
        rclone authorize &quot;drive&quot; &quot;eyJzY29wZSI6ImRyaXZlIn0&quot;
Then paste the result.
Enter a value.
config_token&gt; 
</code></pre><p>需要切换到本地电脑终端，在本地终端运行下面命令。浏览器会弹出来让你授权。
正常授权即可</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">rclone authorize &quot;drive&quot; &quot;eyJzY29wZSI6ImRyaXZlIn0&quot;
</code></pre><p>完成授权得到tocken 粘贴到服务器里面
<img src="https://img.dryice.icu/images/2025/11/20/20251120155813856_repeat_1763625496676__432829.png" alt="image.png" height="801" width="1682"/></p><p><img src="https://img.dryice.icu/images/2025/11/20/20251120160105704_repeat_1763625667279__355609.png" alt="image.png"/></p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">Configure this as a Shared Drive (Team Drive)?

y) Yes
n) No (default)
y/n&gt; n
</code></pre><p>这里选 n，因为你用的是个人 Google Drive，不是企业 Workspace 的 Team Drive（共享盘）。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">Configuration complete.
Options:
- type: drive
- scope: drive
- token: {&quot;access_token&quot;:&quot;xxx&quot;,&quot;token_type&quot;:&quot;Bearer&quot;,&quot;refresh_token&quot;:&quot;xxxx&quot;,&quot;expiry&quot;:&quot;2025-11-20T16:58:27.382702815+08:00&quot;,&quot;expires_in&quot;:3599}
- team_drive: 
Keep this &quot;gdrive&quot; remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d&gt; 
</code></pre>
<p>这里直接选 <strong>y</strong> 保存即可；到这你的 <code>gdrive</code> 已经配置完成了。</p><h2 id="">验证</h2><p>接下来你可以直接测试一下：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">rclone ls gdrive:
</code></pre>
<p>可以看见谷歌硬盘里的东西啦。</p><p><img src="https://img.dryice.icu/images/2025/11/20/20251120160611663_repeat_1763625973188__766154.png" alt="image.png"/></p>
<p>挂载硬盘</p><p>我这里是将谷歌云盘的debain文件夹挂在到 <code>/mnt/gdrive</code>，你可以自行调整</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">mkdir -p /mnt/gdrive
</code></pre>
<pre class="language-bash lang-bash"><code class="language-bash lang-bash">sudo tee /etc/systemd/system/rclone-gdrive.service &lt;&lt;&#x27;EOF&#x27;
[Unit]
Description=Rclone Google Drive Mount
After=network-online.target

[Service]
Type=notify
User=root
ExecStart=/usr/bin/rclone mount gdrive:debain /mnt/gdrive \
  --vfs-cache-mode writes \
  --vfs-cache-max-age 24h \
  --vfs-cache-max-size 10G \
  --buffer-size 64M \
  --dir-cache-time 72h \
  --poll-interval 1m \
  --log-file /var/log/rclone-gdrive.log \
  --log-level INFO
ExecStop=/bin/fusermount3 -uz /mnt/gdrive
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF
</code></pre>
<pre class="language-bash lang-bash"><code class="language-bash lang-bash"># 重载 systemd 配置
sudo systemctl daemon-reexec
sudo systemctl daemon-reload

# 启动服务 &amp; 开机自启
sudo systemctl enable --now rclone-gdrive.service

# 查看状态
sudo systemctl status rclone-gdrive
sudo journalctl -u rclone-gdrive -f   # 实时日志
</code></pre>
<p>可以看见挂载成功了。</p><p><img src="https://img.dryice.icu/images/2025/11/20/20251120161510015_repeat_1763626511536__622555.png" alt="image.png"/></p><p>可以用 <code>df -h | grep gdrive</code> 看看挂载情况，嗯，可以看见使用情况。</p><p><img src="https://img.dryice.icu/images/2025/11/20/20251120161931482_repeat_1763626773048__561602.png" alt="image.png"/></p><p>接下来试试在 这个文件夹下面创建文件看看是否同步。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">cd /mnt/gdrive
touch test.txt
</code></pre>
<p>可以看见，能正常同步啦。
<img src="https://img.dryice.icu/images/2025/11/20/20251120162202612_repeat_1763626924255__225517.png" alt="image.png"/></p><p>现在只需要，写定时任务把一些重要的数据做同步就行啦。</p></div><p style="text-align:right"><a href="https://dryice.icu/posts/technology/rclone-mount-gdrive#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://dryice.icu/posts/technology/rclone-mount-gdrive</link><guid isPermaLink="true">https://dryice.icu/posts/technology/rclone-mount-gdrive</guid><dc:creator><![CDATA[ice]]></dc:creator><pubDate>Thu, 20 Nov 2025 08:41:45 GMT</pubDate></item><item><title><![CDATA[Obsidian笔记图床搭建 (MinIO + PicGo)]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120131640298_repeat_1763615802085__906829.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120134232921_repeat_1763617354471__829803.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120143514766_repeat_1763620516368__069447.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120143553098_repeat_1763620554790__417443.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120143611736_repeat_1763620573044__793594.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120142227714_repeat_1763619749176__200654.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120142414295_repeat_1763619855710__663809.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120142832809_repeat_1763620114339__078528.png"/><link rel="preload" as="image" href="https://img.dryice.icu/images/2025/11/20/20251120142855938_repeat_1763620137475__135577.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://dryice.icu/posts/technology/image-hosting-setup">https://dryice.icu/posts/technology/image-hosting-setup</a></blockquote><div><h2 id="">背景</h2><p>最近用的这款笔记，方便倒是很方便，通过git插件能实现笔记的同步，但是图片都是在本地，也同步到github上去了，这本身没啥问题，但是如果我将md文件倒入到到我的博客，这就会有问题了。图片就会无法加载。这里我们就需要用到图床了。</p><h2 id="">前提</h2><p>前提你需要一个域名和服务器。前段时间刷到个可以用<code>cloudflare</code>图床搭建的项目好像什么都不需要，感兴趣可以去看看。</p><p><a href="https://linux.do/t/topic/251653">https://linux.do/t/topic/251653</a></p><p><a href="https://github.com/MarSeventh/CloudFlare-ImgBed">MarSeventh/CloudFlare-ImgBed: CloudFlare 图床，基于 CloudFlare Pages 和 Telegram Bot 的免费图片托管解决方案！ (github.com)</a><br/>从<a href="https://linux.do/t/topic/154391">【白嫖大善人】基于Telegraph的CF免费图床有全新前端啦！！！（开源、清晰、美观、动画丝滑、玩法多样）</a><br/><a href="https://linux.do/t/topic/215977/27">收集大家关于CF和TG BOT图床的意见，为下一步开发做准备</a></p><h2 id="minio">MinIO部署及解析</h2><h3 id="dockerminio">docker部署MinIO</h3><p>最简单的方式就是通过docker部署啦。</p><pre class="language-yml lang-yml"><code class="language-yml lang-yml">services:
  minio:
    image: minio:latest
    container_name: local_minio
    restart: always
    environment:
      MINIO_ROOT_USER: minio_h2tzp6
      MINIO_ROOT_PASSWORD: minio_Sfh2Zr
      TZ: Asia/Shanghai
    volumes:
      - ./data:/data
      - ./config:/root/.minio
    ports:
      - &quot;9000:9000&quot;
      - &quot;9001:9001&quot;
    command: server /data --console-address &quot;:9001&quot; --address &quot;:9000&quot;
</code></pre>
<p>创建一个文件夹下面放上面内容的 <code>docker-compose.yml</code>文件，并创建<code>data</code>和<code>config</code>目录用来外挂数据和配置。执行下面命令启动容器。MinIO就部署好啦。</p><pre class="language-shell lang-shell"><code class="language-shell lang-shell"># 启动容器
docker compose up -d 
</code></pre>
<p>接下来配置反向代理</p><h3 id="">域名解析</h3><p>你可以将域名托管在 Cloudflare 上，或者通过阿里云等国内服务进行域名解析。我选择 Cloudflare 的原因是，它会在你服务器和外部访问之间加一层代理，隐藏真实 IP。如果服务器在境外，还能避免被墙的风险。</p><p>在我的配置中，我创建了两个解析记录：一个用于 MinIO 控制台访问（看板），另一个用于对外的 MinIO 文件访问，这样管理和使用更清晰。</p><p><img src="https://img.dryice.icu/images/2025/11/20/20251120131640298_repeat_1763615802085__906829.png" alt="域名解析" height="88" width="1316"/></p><h3 id="">反向代理</h3><p>我使用的是 <strong>1Panel</strong> 面板，配置反向代理比较简单。大致步骤如下：</p><p><strong>操作路径：</strong></p><p><code>网站 → 网站 → 创建网站 → 反向代理</code></p><p>然后依次填写：</p><ul><li><strong>域名</strong>：刚刚在 DNS 中解析好的二级域名</li><li><strong>代理地址</strong>：MinIO 服务的访问地址</li><li><strong>SSL</strong>：如需 HTTPS，需要提前申请好证书</li></ul><p>提交后即可完成反向代理配置。</p><p>需要注意两点：</p><ol start="1"><li><strong>域名端口</strong>：通常使用 <code>80</code>（HTTP）和 <code>443</code>（HTTPS）</li><li><strong>SSL 配置</strong>：开启 HTTPS 时必须先在 1Panel 里导入或申请证书</li></ol><hr/><h3 id="">示例</h3><p>以我的配置为例：</p><ul><li><strong>MinIO 控制台域名</strong>：<code>minio.xxx.com</code>
<br/><ul><li>代理地址：<code>http://127.0.0.1:9001</code></li></ul></li><li><strong>文件访问域名</strong>：<code>img.xxx.com</code>
<br/><ul><li>代理地址：<code>http://127.0.0.1:9000</code></li></ul></li></ul><p>这样就实现了 MinIO 控制台和文件访问分别通过不同二级域名来访问，既清晰又方便管理。</p><hr/><h2 id="picgo-">PicGo 配置</h2><h3 id="picgo">下载PicGo</h3><p><a href="https://github.com/Molunerfinn/picgo/releases">下载地址</a>,从这个页面选择稳定的版本下载。
安装好之后，下载插件,选择 minio-custom 2.5.3</p><p><img src="https://img.dryice.icu/images/2025/11/20/20251120134232921_repeat_1763617354471__829803.png" alt="image.png"/></p>
<h3 id="">配置图床</h3><p>在这个里面配置 endPoint，其余的正常填写即可，需要注意一下这里的，自定义域名也最好写一下，不然返回的地址会带一个443端口很不好看。
启用自动归档，这里会自动创建年月日的文件夹，方便查找。</p><p>对了，还需要在MinIO创建对于的Bucket,和accessKey,将Bucket设置为public
<img src="https://img.dryice.icu/images/2025/11/20/20251120143514766_repeat_1763620516368__069447.png" alt="image.png"/></p>
<p>这里的到的AccessKey 和 SecreKey 填写到下面的配置里面</p><p><img src="https://img.dryice.icu/images/2025/11/20/20251120143553098_repeat_1763620554790__417443.png" alt="image.png"/></p><p><img src="https://img.dryice.icu/images/2025/11/20/20251120143611736_repeat_1763620573044__793594.png" alt="image.png"/></p><blockquote>
<p>注意这个endPoint 不需要前面的https</p></blockquote>
<p><img src="https://img.dryice.icu/images/2025/11/20/20251120142227714_repeat_1763619749176__200654.png" alt="image.png"/></p><p><img src="https://img.dryice.icu/images/2025/11/20/20251120142414295_repeat_1763619855710__663809.png" alt="image.png"/></p>
<h2 id="obsidian">Obsidian配置</h2><p>下载插件</p><p><img src="https://img.dryice.icu/images/2025/11/20/20251120142832809_repeat_1763620114339__078528.png" alt="image.png"/></p><p>配置上传地址接口，这里的地址和PicGo里的核对一下是不是这个端口，正常来说不用改，直接就能使用，如果用不了报错，看看是不是缺了依赖，我用的是linux，缺少一个<code>wl-clipboard</code>的依赖。安装一下即可。</p><pre class=""><code class="">Error: Can&#x27;t find no wl-clipboard at Socket.&lt;anonymous&gt; (/tmp/.mount_PicGo-VGUMov/resources/app.asar/node_modules/picgo/dist/index.cjs.js:1:50431) at Socket.emit (node:events:394:28) at addChunk (node:internal/streams/readable:315:12) at readableAddChunk (node:internal/streams/readable:289:9) at Socket.Readable.push (node:internal/streams/readable:228:10) at Pipe.onStreamRead (node:internal/stream_base_commons:199:23)
</code></pre>
<table><thead><tr><th> 发行版                            </th><th> 安装命令                            </th></tr></thead><tbody><tr><td> <strong>Fedora / Red Hat / CentOS</strong>​ </td><td> <code>sudo dnf install wl-clipboard</code> </td></tr><tr><td> <strong>Debian / Ubuntu</strong>​           </td><td> <code>sudo apt install wl-clipboard</code> </td></tr><tr><td> <strong>Arch Linux / Manjaro</strong>​      </td><td> <code>sudo pacman -S wl-clipboard</code>   </td></tr></tbody></table>
<p><img src="https://img.dryice.icu/images/2025/11/20/20251120142855938_repeat_1763620137475__135577.png" alt="image.png"/></p>
<p>好啦，现在在Obsidian里面粘贴图片直接会到图床上了。</p></div><p style="text-align:right"><a href="https://dryice.icu/posts/technology/image-hosting-setup#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://dryice.icu/posts/technology/image-hosting-setup</link><guid isPermaLink="true">https://dryice.icu/posts/technology/image-hosting-setup</guid><dc:creator><![CDATA[ice]]></dc:creator><pubDate>Thu, 20 Nov 2025 06:42:14 GMT</pubDate></item></channel></rss>