<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>LLM on Nekonull's Garden</title><link>https://nekonull.me/tags/llm/</link><description>Recent content in LLM on Nekonull's Garden</description><generator>Hugo</generator><language>zh-CN</language><copyright>CC-BY-SA-4.0</copyright><lastBuildDate>Sun, 21 Sep 2025 22:58:00 +0800</lastBuildDate><atom:link href="https://nekonull.me/tags/llm/index.xml" rel="self" type="application/rss+xml"/><item><title>软文检测器</title><link>https://nekonull.me/posts/is-it-soft-ad/</link><pubDate>Sun, 21 Sep 2025 22:58:00 +0800</pubDate><guid>https://nekonull.me/posts/is-it-soft-ad/</guid><description>&lt;p&gt;读某些新闻网站或公众号的时候，常常会越读越有种隐约的广告感（&amp;ldquo;这是广告吗？&amp;quot;），直到读到文末才彻底验证（”这果然就是广告吧！“）。之前买了 GLM 20 元一个月的 coding 套餐（agent 用的是 claude code），那就顺手 vibe coding 一个小工具来识别吧。于是就有了”软文检测器“。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在线访问: &lt;a href="https://nekonull.me/is-it-soft-ad/"&gt;https://nekonull.me/is-it-soft-ad/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Github: &lt;a href="https://github.com/jerrylususu/is-it-soft-ad"&gt;https://github.com/jerrylususu/is-it-soft-ad&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个小 app 本身并没有什么特别值得说道的，毕竟只是顺手做的玩具，代码我也没有仔细审查，看起来似乎没啥问题而且也能跑就提交了。作为一个 llm wraper，基本上就是一个 fastapi 后端加一个很简单的 vanilla js 前端（甚至没有用框架），前端提交内容后扔给 llm，然后用 sse 推送响应，来避免前端长时间卡住不出结果。模型上用的是 gemini-flash-2.0，是我能找到的速度-质量-价格最佳平衡点，而且 gemini flash 本身就极其便宜了，极大缓解了我的 llm 用量焦虑。prompt 是 deepseek r1 写的，在 repo 的 json 文件里可以找到，生成后稍微人工调整了一下但没有大改。&lt;/p&gt;
&lt;p&gt;有人可能会好奇，在 llm 生成响应的过程中，返回的 json 并不完整，前端是如何保证展示有效内容的呢。实际上这里参考了苹果 wwdc 25 的 &lt;a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-286-meet-the-foundation-models-framework/#Snapshot-streaming"&gt;Snapshot streaming&lt;/a&gt; 的做法，虽然 llm 返回的响应并不是完整的 json，但是有库可以尝试从不完整的 json 解析出有效的 json，我用的是 &lt;a href="https://github.com/promplate/partial-json-parser"&gt;promplate/partial-json-parser&lt;/a&gt;（原理大概是自定义一个更宽松的parser） ，在后端包一层，就可以确保前端每次 sse 拿到的 event 都是有效 json了（虽然可能部分字段不完整）。&lt;/p&gt;
&lt;p&gt;自己稍微玩了玩，确认没啥问题了之后，就开始想怎么部署到公网上。vibe coding 极大简化了写代码（如果你不在乎这个项目的长期可维护性，只是想创建个能用的 mvp）；但是 vibe deploying 并没有成熟度能与之相当的工具链。不少人用 supabase，但是对我这样一个简单的 llm wrapper 来说未免有点太重了，部署在别人的平台上也有点感觉不太安心（担心哪天超过 free tier 被收钱，虽然概率极其之小）。自己部署的话，又有不少前期工作需要做，例如子域名、网关、https、daemon化等等。我之前大部分分享出去的小工具都是纯前端的，扔在 github pages 上就完事了，最多写个 yaml 配置下 ci，带后端的对外工具实际上这还是第一个。最后分享欲还是战胜了嫌麻烦的心态，决定后端扔在一个不怎么用的云服务器上（服务器本身的开销可以忽略不计），前端还是放 github pages。最后满打满算下来，这个项目写代码&amp;amp;自测的时间，可能还没有准备部署环境&amp;amp;实际部署的时间长。&lt;/p&gt;
&lt;p&gt;关于 llm 的应用，其实还涉及到一个控制用量（aka 限额）的问题。虽然我倒是不太介意在这种小玩具上每个月花点小钱（一瓶无糖可乐的价格都够一个便宜 llm 模型用很久了），但是我不想失去控制我能花多少钱的能力。一开始想的是在后端自己写个限额，或者直接用现成的库，但感觉涉及到钱的话还是有些危险，于是搁置了。手写限额的话，还需要考虑状态存储，就又多引入了一层复杂性（例如 cloudflare worker kv 每天只有 1k 写额度，上 durable object 或者 d1 也太重了。）顺带吐槽下，国内好几家主流的 LLM 服务提供商，都没有 by api key 限制使用额度的能力，但是在国外（如 openrouter）或各类中转商，这是基础的不能再基础的能力了。最后找了个支持 api key 限额的中转服务商，才算放下心来，知道再怎么刷也不会把我的账户刷爆了。&lt;/p&gt;
&lt;p&gt;最后是可观测性，出于一些小小的私心，还是希望看看有多少人用过。为此前端加了 google analytics，后端用 opentelemetry 上报 trace 到 uptrace （他们家免费额度很宽裕）。最后实际上因为没怎么做推广（只在两个论坛上发了简单介绍），当然也没啥人用，最后大概是一天10个请求的样子，我自己的请求可能就占了一半。&lt;/p&gt;
&lt;p&gt;一些未来的可能优化点（如果我还有兴趣在这个项目上继续投入的话）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把单纯的是否软文改成一个连续的光谱，或者至少多增加几个层级&lt;/li&gt;
&lt;li&gt;现在对软文敏感度太高了，正常内容可能也会被认为是软文，看看怎么改 prompt 优化下&lt;/li&gt;
&lt;li&gt;调查为啥得加 await asyncio.sleep 才能让 sse 正常工作&lt;/li&gt;
&lt;li&gt;深入分析，补充到文内的引用&lt;/li&gt;
&lt;li&gt;对文章按照”推广程度“染色（说实话我不太确定技术上这怎么实现）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外还有一些想法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可能对高级用户 bring your own key 的做法会更好，还能省去我部署后端的开发&amp;amp;运营复杂度；可能可以考虑把这个项目转换成一个支持自己提供 llm 接入和使用现成后端的模式？
&lt;ul&gt;
&lt;li&gt;进一步地，是否存在这样的生态系统，提供了统一的 llm 接口（例如一个 js/ts sdk），用户只需要装一个插件，在插件内配置一次，各个网站就可以直接使用用户已配置的 llm 服务？（似乎 chrome 有个内置的小模型，但是很多事情小模型还是不太够用）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;之前试过扣子空间，虽然搭建简单原型没问题，但是分享起来异常困难（例如有审核流程）；如果存在更简单的分享&amp;amp;创作方式，可能这类小但是有意义的 llm wrapper 会更广泛？&lt;/li&gt;
&lt;li&gt;与其分享一个又一个 llm wrapper ，不如分享结构化 prompt 和支持运行结构化 prompt 的工具？（例如另一个之前的小创作 &lt;a href="https://pages.nekonull.me/llm-prompt-templater.html"&gt;llm-prompt-templater&lt;/a&gt; ）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这篇文章有点混乱，想到哪里就写到哪里了，只是我个人的 brain dump。如果你能读到这里，那真是辛苦了。&lt;/p&gt;</description></item><item><title>让 Claude Code 使用 tmux/screen 执行交互式操作</title><link>https://nekonull.me/share/claude-code-screen-tmux/</link><pubDate>Wed, 13 Aug 2025 00:45:01 +0800</pubDate><guid>https://nekonull.me/share/claude-code-screen-tmux/</guid><description>&lt;p&gt;起因是逛 YouTube 时看到了 &lt;a href="https://www.youtube.com/@ArminRonacher"&gt;Armin Ronacher&lt;/a&gt; 的这两个视频：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=chSL6p5u-74"&gt;Claude Runs LLDB in Screen&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=tg61cevJthc"&gt;Claude Code Debugs with LLDB in TMUX&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看起来很酷炫，但实际上原理很简单：screen/tmux 都支持通过命令行传递输入并返回当前正在展示的内容，只需要写个文档让 Claude Code 用就好了（见文末）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Screen: &lt;code&gt;-X stuff&lt;/code&gt; 输入，&lt;code&gt;-X hardcopy&lt;/code&gt; 输出&lt;/li&gt;
&lt;li&gt;Tmux: &lt;code&gt;send-keys&lt;/code&gt; 输入，&lt;code&gt;capture-pane&lt;/code&gt; 输出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;甚至还可以 attach 上去看看在做什么。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Screen: &lt;code&gt;screen -r ${session_name}&lt;/code&gt; (Ctrl+A D 释放)&lt;/li&gt;
&lt;li&gt;Tmux: &lt;code&gt;tmux attach-session -t ${session_name}&lt;/code&gt; (Ctrl+B D 释放)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两者相比，tmux 似乎更好一些，因为 screen 只能输出到文件，llm 还需要再 read 一次；tmux 的输出直接在 stdout，不用多绕一道。&lt;/p&gt;
&lt;p&gt;这是一个 GIF 示例，展示了 Claude Code 在 tmux 中使用 gdb 解决经典的 bomb lab 问题。（加速 10x）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这只是一个通用能力的展示；如果你真的想要用 gdb，可能 &lt;a href="https://github.com/signal-slot/mcp-gdb"&gt;mcp-gdb&lt;/a&gt; 是更好的选择。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;img class="td-image" src="https://nekonull.me/img/claude-code-tmux.gif" alt="Demo" data-td-image-zoom loading="lazy" decoding="async"&gt;&lt;hr&gt;
&lt;p&gt;完整 prompt：&lt;/p&gt;
&lt;pre&gt;
# Debugging Guide for AI Agents
This guide provides instructions for debugging using `gdb` within terminal multiplexers. It covers two options: `screen` and `tmux`. Use these multiplexers to run a command in a detached session, allowing for input sending, output capturing, and session management. Sessions are named using `${session_name}` for consistency.
Sessions can be started with a specific command (e.g., `gdb`) or empty, with commands sent later. Examples assume a session running `gdb`.
## Using Screen
- **Starting the Session**: Start a detached `screen` session named `${session_name}`. Use flags like `-S` (session name), `-d` (detach), and `-m` (create even if detached). The `${command}` can be `gdb [optional-program]` or any other command.
```bash
screen -S ${session_name} -d -m ${command}
# Example: screen -S my-debug-session -d -m gdb my_program
# Example: screen -S my-shell-session -d -m bash
```
- **Sending Input**: Send strings or commands into the session using the `stuff` command. Use `$'\n'` for newlines.
```bash
screen -S ${session_name} -X stuff $'${input_string}\n'
# Example: screen -S my-debug-session -X stuff $'run &lt; /usr/share/dict/words\n'
# Example: screen -S my-shell-session -X stuff $'ls -l\n'
```
- **Reading Output**: Screen does not output directly to stdout, so use a workaround: Capture the session output to a text file using `-X hardcopy`, then read the file.
```bash
screen -S ${session_name} -X hardcopy tmp_screen_out.txt
cat tmp_screen_out.txt
```
- **Closing the Session**: When debugging is finished, quit the session.
```bash
screen -S ${session_name} -X quit
```
## Using Tmux
- **Starting the Session**: Start a detached `tmux` session named `${session_name}`. Use `new-session` with `-s` (session name) and `-d` (detach). The `${command}` can be `gdb [optional-program]` or any other command.
```bash
tmux new-session -s ${session_name} -d ${command}
# Example: tmux new-session -s my-debug-session -d gdb my_program
# Example: tmux new-session -s my-shell-session -d bash
```
- **Sending Input**: Send strings or commands into the session using `send-keys`. Use `C-m` for Enter (newline).
```bash
tmux send-keys -t ${session_name} '${input_string}' C-m
# Example: tmux send-keys -t my-debug-session 'run &lt; /usr/share/dict/words' C-m
# Example: tmux send-keys -t my-shell-session 'pwd' C-m
```
- **Reading Output**: Capture the pane output directly to stdout using `capture-pane` with `-p`.
```bash
tmux capture-pane -t ${session_name} -p
```
- **Closing the Session**: When debugging is finished, kill the session.
```bash
tmux kill-session -t ${session_name}
```
```
&lt;/pre&gt;</description></item><item><title>让 Claude Code 使用其他模型</title><link>https://nekonull.me/share/claude-code-3rd-party-model/</link><pubDate>Sun, 22 Jun 2025 19:43:02 +0800</pubDate><guid>https://nekonull.me/share/claude-code-3rd-party-model/</guid><description>&lt;p&gt;最近在尝试各类 agent 项目，大部分都支持使用任何 OpenAI 兼容 API 格式的模型提供商，但是作为这个流派最著名的 Claude Code 却只能用自家模型。自家模型其实也不错，但是对我这种无法在生产环境使用，只是用来自己探索和 side project 的场景下太贵了。搜了下似乎没有人介绍过如何让 Claude Code 和非官方模型配合使用，于是决定记录下。&lt;/p&gt;
&lt;p&gt;这一方法的核心是 &lt;a href="https://github.com/badlogic/lemmy/tree/main/apps/claude-bridge"&gt;Claude Bridge&lt;/a&gt; 这个项目。实现上，和任何计算机科学问题的解决方式一样，加了一个中间层。具体而言，是 patch 了 node 的 fetch 方法，拦截所有向 Anthropic 官方的请求，转换成标准的 OpenAI 格式（其实是作者自己的一个统一 format），调用指定的提供商，在流式响应的时候再转换回 Claude 的 SSE 格式。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="td-code td-code--untitled" id="td-code-004d36b0-fence-0" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="5"&gt;&lt;div class="td-code__utilities td-code__utilities--compact"&gt;
&lt;button class="td-code__copy" type="button" hidden data-td-code-copy
data-td-code-target="td-code-004d36b0-fence-0" data-td-copy-mode="all"
data-td-label-copy="复制代码"
data-td-label-copied="已复制"
data-td-label-error="复制失败"
aria-label="复制代码" title="复制代码"&gt;
&lt;i class="fa-regular fa-copy" aria-hidden="true"&gt;&lt;/i&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;div class="td-code__viewport" id="td-code-004d36b0-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 官方的 claude-code&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm install -g @anthropic-ai/claude-code
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# claude-bridge&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm install -g @mariozechner/claude-bridge&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;span class="visually-hidden" role="status" aria-live="polite" data-td-code-status&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;ol start="2"&gt;
&lt;li&gt;准备 API Key 提供脚本&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这一步参考了 Claude Code 的一个 issue &lt;a href="https://github.com/anthropics/claude-code/issues/441"&gt;How can I use my API key without signing in?&lt;/a&gt;，以及这篇文章 &lt;a href="https://przbadu.hashnode.dev/setting-up-claude-code-with-api-key"&gt;Setting up Claude-Code with API Key&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;本步骤解决的问题是，Claude Code 默认情况下只能以 Claude 账号的形式登录（重定向到官方登录页），但不能在不登入账号的情况下直接使用 API Key 发起请求。然而实际上有办法绕过这一限制，只需要创建一个 &lt;code&gt;apiKeyHelper&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;首先在 &lt;code&gt;~/.claude/settings.json&lt;/code&gt; 中增加如下内容。（文件没有的话可以先创建）&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-004d36b0-fence-1" data-td-code data-td-code-auto-id
data-td-language="json" data-td-line-count="3"&gt;&lt;div class="td-code__utilities td-code__utilities--compact"&gt;
&lt;button class="td-code__copy" type="button" hidden data-td-code-copy
data-td-code-target="td-code-004d36b0-fence-1" data-td-copy-mode="all"
data-td-label-copy="复制代码"
data-td-label-copied="已复制"
data-td-label-error="复制失败"
aria-label="复制代码" title="复制代码"&gt;
&lt;i class="fa-regular fa-copy" aria-hidden="true"&gt;&lt;/i&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;div class="td-code__viewport" id="td-code-004d36b0-fence-1-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;apiKeyHelper&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;~/.claude/anthropic_key.sh&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;span class="visually-hidden" role="status" aria-live="polite" data-td-code-status&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;然后创建 &lt;code&gt;~/.claude/anthropic_key.sh&lt;/code&gt;，填入如下内容：&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-004d36b0-fence-2" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="1"&gt;&lt;div class="td-code__utilities td-code__utilities--compact"&gt;
&lt;button class="td-code__copy" type="button" hidden data-td-code-copy
data-td-code-target="td-code-004d36b0-fence-2" data-td-copy-mode="all"
data-td-label-copy="复制代码"
data-td-label-copied="已复制"
data-td-label-error="复制失败"
aria-label="复制代码" title="复制代码"&gt;
&lt;i class="fa-regular fa-copy" aria-hidden="true"&gt;&lt;/i&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;div class="td-code__viewport" id="td-code-004d36b0-fence-2-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sk_your_anthropic_api_key&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;span class="visually-hidden" role="status" aria-live="polite" data-td-code-status&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;（因为我们会用第三方模型提供商，所以这里可以随便填写）&lt;/p&gt;
&lt;p&gt;最后给让这个 shell 脚本可执行。&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-004d36b0-fence-3" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="1"&gt;&lt;div class="td-code__utilities td-code__utilities--compact"&gt;
&lt;button class="td-code__copy" type="button" hidden data-td-code-copy
data-td-code-target="td-code-004d36b0-fence-3" data-td-copy-mode="all"
data-td-label-copy="复制代码"
data-td-label-copied="已复制"
data-td-label-error="复制失败"
aria-label="复制代码" title="复制代码"&gt;
&lt;i class="fa-regular fa-copy" aria-hidden="true"&gt;&lt;/i&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;div class="td-code__viewport" id="td-code-004d36b0-fence-3-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod +x ~/.claude/anthropic_key.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;span class="visually-hidden" role="status" aria-live="polite" data-td-code-status&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;ol start="3"&gt;
&lt;li&gt;运行 Claude Code&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;用目标提供商和模型替换命令中的参数即可。第一个 openai 参数是提供商格式，也可以换成 gemini 等。详情请参考 Claude Bridge 的 readme 文件。&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-004d36b0-fence-4" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="1"&gt;&lt;div class="td-code__utilities td-code__utilities--compact"&gt;
&lt;button class="td-code__copy" type="button" hidden data-td-code-copy
data-td-code-target="td-code-004d36b0-fence-4" data-td-copy-mode="all"
data-td-label-copy="复制代码"
data-td-label-copied="已复制"
data-td-label-error="复制失败"
aria-label="复制代码" title="复制代码"&gt;
&lt;i class="fa-regular fa-copy" aria-hidden="true"&gt;&lt;/i&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;div class="td-code__viewport" id="td-code-004d36b0-fence-4-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;claude-bridge openai &lt;span class="o"&gt;{{&lt;/span&gt;model_name&lt;span class="o"&gt;}}&lt;/span&gt; --baseURL &lt;span class="o"&gt;{{&lt;/span&gt;base_url&lt;span class="o"&gt;}}&lt;/span&gt; --apiKey &lt;span class="o"&gt;{{&lt;/span&gt;api_key&lt;span class="o"&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;span class="visually-hidden" role="status" aria-live="polite" data-td-code-status&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;当然这一方法也不是万能的，有一些已知限制：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;token 计数不准&lt;/li&gt;
&lt;li&gt;不能输入图片&lt;/li&gt;
&lt;li&gt;网络搜索/fetch 不可用&lt;/li&gt;
&lt;li&gt;thinking/reasoning 部分可能无法被正常解析&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;（之后可能会写一篇文章对比不同的 agent，但是得看有没有时间了&amp;hellip;）&lt;/p&gt;</description></item><item><title>LLM x 书签收藏：摘要 &amp; 全文索引</title><link>https://nekonull.me/posts/llm_x_bookmark/</link><pubDate>Mon, 07 Oct 2024 13:52:00 +0800</pubDate><guid>https://nekonull.me/posts/llm_x_bookmark/</guid><description>&lt;h2 id="背景"&gt;背景&lt;a class="td-heading-self-link" href="#%e8%83%8c%e6%99%af" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;网上冲浪的时候，经常会遇到一些有趣的文章或者网站，让我有收藏起来以备后用的冲动（虽然绝大部分情况下都没有再用过）。然而一个人收藏未免有些太孤单了，因此自从 2021 年 5 月以来，我一直在使用一个名为 &lt;a href="https://github.com/osmoscraft/osmosmemo"&gt;osmos::memo&lt;/a&gt; 的书签插件，将我的收藏直接记录到一个公开的 &lt;a href="https://github.com/jerrylususu/bookmark-collection"&gt;Github 存储库&lt;/a&gt;。这个插件的工作原理很简单，首先设置好 Github 的 token，然后每次点击收藏按钮都会在浏览器里临时 clone 一次，追加新收藏的条目到文件顶部，生成 commit 并提交，然后推送回 Github。但是这样简单的工作流程也十分有效，除了 token 过期的时候需要手动续期（过期前有 Github 自带的邮件提醒，所以基本上不会拖到最后），没有什么可以出错的空间，近三年半下来也已经帮我积累了 800+ 条目了。&lt;/p&gt;
&lt;h2 id="问题"&gt;问题&lt;a class="td-heading-self-link" href="#%e9%97%ae%e9%a2%98" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;然而目前的书签收藏流程中，依然会存在一些问题。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;书签指向的 URL 可能不再存在（例如某个博客的主人决定不再续费域名，或者是做了链接格式的调整），导致成为悬空的死链接&lt;/li&gt;
&lt;li&gt;目前的记录项只有书签的 URL、标题和可选的标签（而且我打标签的习惯不太好，光靠标签基本上不太能找到），导致查找的时候如果对关键词记忆不清楚，很有可能找不到&lt;/li&gt;
&lt;li&gt;书签里一大部分是长文章，时间一久很有可能忘记内容，如果只是临时找些东西，通读一次又略微有些费时费事，导致查找&amp;amp;引用效率下降。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="解决"&gt;解决&lt;a class="td-heading-self-link" href="#%e8%a7%a3%e5%86%b3" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;为了解决这些问题，我建立了一个新的存储库 &lt;a href="https://github.com/jerrylususu/bookmark-summary"&gt;bookmark-summary&lt;/a&gt;。这个存储库可以视为现有书签存储库的辅助数据，其中包含了新增书签的 Markdown 格式全文、列表摘要、一句话总结，和现有存储库之间通过 Github Actions 联动。其工作原理如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我通过书签插件，在现有的书签存储库中新增了一个条目&lt;/li&gt;
&lt;li&gt;代码提交到主干，触发名为 &lt;code&gt;summarize&lt;/code&gt; 的 Github Actions（&lt;a href="https://github.com/jerrylususu/bookmark-collection/blob/main/.github/workflows/bookmark_summary.yml"&gt;yml 工作流文件&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;Github Actions 执行，首先 checkout 书签存储库和摘要存储库，然后执行 &lt;a href="https://github.com/jerrylususu/bookmark-summary/blob/main/process_changes.py"&gt;process_changes.py&lt;/a&gt;
&lt;ol&gt;
&lt;li&gt;首先解析书签 README.md 文件，找到最近新增的条目标题和 URL&lt;/li&gt;
&lt;li&gt;将 URL 保存到 Wayback Machine&lt;/li&gt;
&lt;li&gt;输入 URL，使用 &lt;a href="https://jina.ai/reader/"&gt;jina reader&lt;/a&gt; API 获取网址的 Markdown 全文，并保存到 &lt;code&gt;YYYYMM/{title}_raw.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;输入 URL，使用 LLM 生成列表摘要（prompt 在 &lt;code&gt;summarize_text&lt;/code&gt; 函数 &lt;a href="https://github.com/jerrylususu/bookmark-summary/blob/main/process_changes.py#L80"&gt;link&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;输入列表摘要，使用 LLM 生成一句话总结&lt;/li&gt;
&lt;li&gt;将列表摘要和一句话总结保存到 &lt;code&gt;YYYYMM/{title}.md&lt;/code&gt;（&lt;a href="https://github.com/jerrylususu/bookmark-summary/blob/main/202410/2024-10-02-a-local-first-case-study-jakelazaroff.com.md"&gt;效果示例&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;更新摘要存储库的 README.md，增加到摘要文件的链接&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Github Actions 提交变更到摘要存储库&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里的主要代码基本都是 Claude 和 GPT4o 写的，人肉做了一些小调整。后面随着使用又逐步发现了一些 bug，最近还用 o1-mini &lt;a href="https://github.com/jerrylususu/bookmark-summary/issues/6"&gt;修复了一个&lt;/a&gt;，算是真切感受到了 LLM 对生产力的巨大提升。目前摘要生成用的是深度求索的 deepseek-chat，便宜是真便宜（输入 1元/1M token，输出 2元/1M token，在这个场景下的成本基本上是每个月1元不到），效果也还算可以。&lt;/p&gt;
&lt;h2 id="未来"&gt;未来&lt;a class="td-heading-self-link" href="#%e6%9c%aa%e6%9d%a5" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;最后是一些已知问题，以及未来可能的优化方向。当然和其他所有项目一样，欢迎 fork &amp;amp; PR。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;列表摘要质量：可能是 prompt 的问题，列表摘要倾向于每个大点下面只列两个小点，且没有充分合并需要合并的论点；可能需要考虑进一步优化 prompt，或者换用其他模型（不过我拿便宜的模型都试了一轮，基本上都存在类似问题）&lt;/li&gt;
&lt;li&gt;数据结构化：目前摘要存储库下有个简单的 data.json，但是核心的摘要和全文内容依然是 Markdown 存储的，而不是 JSON 这类程序友好的结构化存储。可能需要考虑在 Markdown 之外另外维护一个 JSON，以备未来的查询。&lt;/li&gt;
&lt;li&gt;代码整理和重构：目前所有逻辑都混在一个大的 Python 文件里，修改和测试起来都很烦人（实际上没有特别好的办法手动测试，目前都得靠手动注释掉部分代码）。未来一个考虑是做重构（o1-mini也给出过比较好的重构结构）+补充测试；另一个是改进书签存储库和摘要存储库的交互方式，例如通过读 git log 或者是明确传递最近书签条目的方式来触发摘要生成，而不是靠目前读文件对比的方式&lt;/li&gt;
&lt;li&gt;向量搜索：目前虽然原文和摘要都存下来了，搜索却还是只能靠基本的文本匹配；可能可以考虑接个 embedding 模型自动生成下嵌入，存到一个 SQLite 数据库（或者用各种向量数据库 as a Service）；主要是查询的时候也得生成 embedding，英文还有小模型可以搞，中文的模型都太大了，没法直接在前端跑不依赖后端服务，这里还得再仔细想想。&lt;/li&gt;
&lt;li&gt;自动生成每周周报：&lt;del&gt;既然现在书签有时间信息，可以考虑每周新增的书签+原文+摘要全部往 LLM 扔，自动生成一个每周摘要，放在 Github Release 里（不过不知道有没有人愿意看就是了）&lt;/del&gt; (已完成，参见 &lt;a href="https://github.com/jerrylususu/bookmark-summary/releases"&gt;Releases&lt;/a&gt;，实现见 &lt;a href="https://github.com/jerrylususu/bookmark-summary"&gt;build_weekly_release.py&lt;/a&gt;，代码主要由 o1-mini 实现)&lt;/li&gt;
&lt;li&gt;改用更现代的工具链：例如 uv，以及把依赖写在 Python 代码头部（PEP 723 &lt;a href="https://packaging.python.org/en/latest/specifications/inline-script-metadata/#inline-script-metadata"&gt;Inline Script Metadata&lt;/a&gt;）？&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="我也想要"&gt;我也想要&lt;a class="td-heading-self-link" href="#%e6%88%91%e4%b9%9f%e6%83%b3%e8%a6%81" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;可以参考以下步骤，在自己的 Github 账户下部署一套类似的系统。（根据回忆写的，所以可能不太详尽）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;参考 &lt;a href="https://github.com/osmoscraft/osmosmemo"&gt;osmos::memo&lt;/a&gt; 的指引，初始化书签存储库（我的叫做 bookmark-collection），安装浏览器插件，并连接到 Github&lt;/li&gt;
&lt;li&gt;新建一个摘要存储库（我的叫做 bookmark-summary），并在其中添加一个空的 README.md 文件&lt;/li&gt;
&lt;li&gt;将 &lt;a href="https://github.com/jerrylususu/bookmark-summary/blob/main/process_changes.py"&gt;process_changes.py&lt;/a&gt; 添加到摘要存储库，用实际的存储库名修改 &lt;code&gt;BOOKMARK_COLLECTION_REPO_NAME&lt;/code&gt; 和 &lt;code&gt;BOOKMARK_SUMMARY_REPO_NAME&lt;/code&gt;；如果需要的话，可以调整 &lt;code&gt;summarize_text&lt;/code&gt; 和 &lt;code&gt;one_sentence_summary&lt;/code&gt; 中的 prompt&lt;/li&gt;
&lt;li&gt;回到书签存储库，将 &lt;a href="https://github.com/jerrylususu/bookmark-collection/blob/main/.github/workflows/bookmark_summary.yml"&gt;bookmark_summary.yml&lt;/a&gt; 添加到 &lt;code&gt;.github/workflows/bookmark_summary.yml&lt;/code&gt;，用 &lt;code&gt;Github账号/书签存储库名&lt;/code&gt; 替换 22 行 &lt;code&gt;jerrylususu/bookmark-collection&lt;/code&gt;，用 &lt;code&gt;Github账号/摘要存储库名&lt;/code&gt; 替换 27 行 &lt;code&gt;jerrylususu/bookmark-summary&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新建一个 PAT（Personal Access Token）
&lt;ul&gt;
&lt;li&gt;入口：Github 主页 - 右上角 Settings - 左侧列表底部 Developer Settings - 左侧列表 Personal Access Token / Fine-grained Tokens - 右侧 Generate New Token - 验证密码&lt;/li&gt;
&lt;li&gt;Token Name: 随便写&lt;/li&gt;
&lt;li&gt;Expiration：可以长一些，但是不能超过 1 年&lt;/li&gt;
&lt;li&gt;Repository access：选 Only select repositories，然后在下面选中自己的摘要存储库&lt;/li&gt;
&lt;li&gt;Permissions：点开 Repository Permissions，找到 Contents，选择 Read and write；其他不用动&lt;/li&gt;
&lt;li&gt;点击底部 Generate Token；Token 只会显示一次，复制下来保存好&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;回到书签存储库，添加密钥到环境变量
&lt;ul&gt;
&lt;li&gt;入口：书签存储库 - 顶部 Settings - 左侧 Secrets &amp;amp; Variables / Actions - Repository secrets - New Repository Secret&lt;/li&gt;
&lt;li&gt;需要添加 4 个（其实有的可以放在 Environments 里，不过这里我为了方便先全放到 Secrets 里了）；冒号前面的是名字，冒号后面的是内容&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;PAT ：第 5 步生成的 token&lt;/li&gt;
&lt;li&gt;OPENAI_API_MODEL ： 模型名，如 gpt-4o-mini；如果像我一样用 deepseek 则填写 deepseek-chat&lt;/li&gt;
&lt;li&gt;OPENAI_API_KEY ： API key，通常以 sk- 开头&lt;/li&gt;
&lt;li&gt;OPENAI_API_ENDPOINT ： 模型 API 地址，留空默认用 OpenAI 官方；可以填中转站；用 deepseek 则填写 &lt;code&gt;https://api.deepseek.com/chat/completions&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;至此应该配置完成了。可以用 osmos::memo 扩展添加一个书签试试，观察书签存储库中工作流是否正常运行，摘要存储库中是否生成了对应的摘要。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="勘误"&gt;勘误&lt;a class="td-heading-self-link" href="#%e5%8b%98%e8%af%af" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;感谢评论区 a1032077316 指出「我也想要一节」中的部分步骤错误，已经在文中修复&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>我的 AI Prompt</title><link>https://nekonull.me/share/my-ai-prompts/</link><pubDate>Sun, 29 Oct 2023 20:17:00 +0800</pubDate><guid>https://nekonull.me/share/my-ai-prompts/</guid><description>&lt;p&gt;记录下自己自用的一些 Prompt。只是迭代下来感觉还不错，但不一定是最好的。如有推荐欢迎回复补充。&lt;/p&gt;
&lt;h2 id="通用场景"&gt;通用场景&lt;a class="td-heading-self-link" href="#%e9%80%9a%e7%94%a8%e5%9c%ba%e6%99%af" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;（主要配合 GPT-3.5-Turbo 使用，回答代码类问题）&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-f1c032c2-fence-0" data-td-code data-td-code-auto-id
data-td-language="" data-td-line-count="1"&gt;
&lt;div class="td-code__viewport" id="td-code-f1c032c2-fence-0-viewport" data-td-code-viewport&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;You are a helpful assistant and also a professional &amp;amp; experienced developer. You can help me by answering my questions. You can also ask me questions. If you are given code related questions, please answer in a consise manner, give code examples with less explaination.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="摘要总结英文"&gt;摘要总结（英文）&lt;a class="td-heading-self-link" href="#%e6%91%98%e8%a6%81%e6%80%bb%e7%bb%93%e8%8b%b1%e6%96%87" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;（主要配合 claude-instant-v1 使用，用于文章摘要、结构化总结）&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-f1c032c2-fence-1" data-td-code data-td-code-auto-id
data-td-language="" data-td-line-count="17"&gt;
&lt;div class="td-code__viewport" id="td-code-f1c032c2-fence-1-viewport" data-td-code-viewport&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;You are a professional reader and analyst. Please summarize the following article in a organized manner. Use markdown list format with indentation indicating the layering of ideas. Ignore any text that is unrelated to the main article. Also include a short tl;dr summary (no more than 50 words and 3 sentences). Refer to the following example when summarizing.
---
[Example Output]
TL;DR summary: Summary in no more than 50 words.
# Title
## Heading 1
- Idea 1
- Reason 1
## Heading 2
1. Numbered Item 1
2. Numbered Item 2
## Conclusion&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="摘要总结中文"&gt;摘要总结（中文）&lt;a class="td-heading-self-link" href="#%e6%91%98%e8%a6%81%e6%80%bb%e7%bb%93%e4%b8%ad%e6%96%87" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;（主要配合 ChatGLM-Turbo 使用，用于文章摘要）&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-f1c032c2-fence-2" data-td-code data-td-code-auto-id
data-td-language="" data-td-line-count="1"&gt;
&lt;div class="td-code__viewport" id="td-code-f1c032c2-fence-2-viewport" data-td-code-viewport&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;您是一名专业的读者和分析者，现在请对下面的文章进行整理总结。使用Markdown列表格式输出总结，每个列表项是一个想法，并用缩进表示思想的层次，更深层的列表项代表论据或想法的演进，忽略任何与主文无关的文字。&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>用 mitmproxy 让 ChatGLM 适配 OpenAI 接口</title><link>https://nekonull.me/share/try-chatglm/</link><pubDate>Sun, 29 Oct 2023 19:41:00 +0800</pubDate><guid>https://nekonull.me/share/try-chatglm/</guid><description>&lt;p&gt;最近看到了几篇关于智谱 AI 的推送文章，才想起来他们的大模型（ChatGLM 系）已经上线好久了。回想 6B 模型刚公布的那会还在 AutoDL 上自己跑过，不过因为模型本身太小，所以其实能做的并不算多。注册了个开发者账户看了看文档，目前可以广泛使用的是 &lt;a href="https://open.bigmodel.cn/pricing"&gt;ChatGLM-Turbo&lt;/a&gt;，上下文窗口 32k token，定价 0.005 元/千token，还是很便宜的。更不用说因为 GLM 系模型以中文语料为主，所以同等长度的中文文本，用 GLM 的 token 消耗比用 GPT 系列的 token 消耗会小很多（测试下来大概在 4x 左右）。&lt;/p&gt;
&lt;p&gt;官网的 Playground 玩了一会感觉还不错，生成的中文明显感觉更自然，没有 GPT 系那么浓烈的翻译腔，于是想着怎么接入到我自己用的客户端 &lt;a href="https://github.com/Bin-Huang/chatbox"&gt;Chatbox&lt;/a&gt; 中日常使用。Chatbox 有内置的 ChatGLM 支持，一般直接设置下 token 就可以了。但是因为我主要用的还是 GPT 系模型，而 Chatbox 又只能全局设置一个 API 服务器字段，所以如果要同时使用 GPT 和 ChatGLM 的话，还是得用之前提到的 mitmproxy，手动完成请求的中转（没有什么是加一个抽象层不能解决的）。这里用 mitm 方式让 GLM 适配 GPT 接口还有个额外的好处，那就是只支持 OpenAI 的第三方应用也可以自动支持 GLM 了（虽然我还没这么用过）。&lt;/p&gt;
&lt;p&gt;和之前适配 OpenRouter 不一样，这次除了修改请求头，还要修改 SSE 响应体。不知道出于什么考虑，GLM 系列模型的响应事件和 GPT 系列的完全不同，修改起来还是有些复杂的。但总之调试了几个小时之后总算是改完了，代码在此：(不建议在生产环境使用，后果自负)&lt;/p&gt;
&lt;p&gt;&lt;a href="https://gist.github.com/jerrylususu/3ebcf6262d110da89ce58d1e8d55bc22"&gt;https://gist.github.com/jerrylususu/3ebcf6262d110da89ce58d1e8d55bc22&lt;/a&gt;&lt;/p&gt;
&lt;script src="https://gist.github.com/jerrylususu/3ebcf6262d110da89ce58d1e8d55bc22.js"&gt;&lt;/script&gt;
&lt;p&gt;改请求头比较简单，修改如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;换 host 和 path&lt;/li&gt;
&lt;li&gt;换 Authorization 头：参考 GLM 开发文档的&amp;quot;鉴权&amp;quot;一节即可（注意这里要用 &lt;code&gt;PyJWT&lt;/code&gt; 库，直接二进制安装的 &lt;code&gt;mitmproxy&lt;/code&gt; 带的 Python 环境不支持安装包，需要走 &lt;code&gt;pipx&lt;/code&gt; 安装，可参考&lt;a href="https://docs.mitmproxy.org/stable/overview-installation/#installation-from-the-python-package-index-pypi"&gt;官方文档&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;消息列表（&lt;code&gt;messages&lt;/code&gt;）修改：GLM 系里叫做 &lt;code&gt;prompt&lt;/code&gt;，而且根据实测只能支持 &lt;code&gt;user&lt;/code&gt;-&lt;code&gt;assistant&lt;/code&gt; 交替，如果存在 &lt;code&gt;system&lt;/code&gt; 或是有两个连续的 &lt;code&gt;user&lt;/code&gt; 消息都会报错；这里稍微转换了下，把所有的非 &lt;code&gt;user&lt;/code&gt;/&lt;code&gt;assistant&lt;/code&gt; 消息都转成 &lt;code&gt;user&lt;/code&gt;，然后手动连接下连续的同 role 消息，保证最后构造的消息列表是两个角色交替。&lt;/li&gt;
&lt;li&gt;开启增量返回：默认似乎是全量返回，这里和 OpenAI 对齐，也改成增量&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;比较烦人的是改响应体，如下所示分别是 GLM 系的返回和 GPT 系的返回。可以发现 GLM 系列比较简单，只有事件类型、流 ID 和增量数据；GPT 系列就更复杂一些，返回的是个 JSON，里面还有嵌套结构。&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-5be1179d-fence-0" data-td-code data-td-code-auto-id
data-td-language="python" data-td-line-count="4"&gt;&lt;div class="td-code__utilities td-code__utilities--compact"&gt;
&lt;button class="td-code__copy" type="button" hidden data-td-code-copy
data-td-code-target="td-code-5be1179d-fence-0" data-td-copy-mode="all"
data-td-label-copy="复制代码"
data-td-label-copied="已复制"
data-td-label-error="复制失败"
aria-label="复制代码" title="复制代码"&gt;
&lt;i class="fa-regular fa-copy" aria-hidden="true"&gt;&lt;/i&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;div class="td-code__viewport" id="td-code-5be1179d-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# GLM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;event:add&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;id:8065135252561182716&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;data:&lt;/span&gt;&lt;span class="se"&gt;\xef\xbc\x8c\n\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# GPT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;data: {&amp;#34;id&amp;#34;: &amp;#34;chatcmpl-8EymH9k9DS9iQQvIH3BguHaZqmib9&amp;#34;, &amp;#34;object&amp;#34;: &amp;#34;chat.completion.chunk&amp;#34;, &amp;#34;created&amp;#34;: 1698580913, &amp;#34;model&amp;#34;: &amp;#34;gpt-3.5-turbo-0613&amp;#34;, &amp;#34;choices&amp;#34;: [{&amp;#34;index&amp;#34;: 0, &amp;#34;delta&amp;#34;: {&amp;#34;content&amp;#34;: &amp;#34;?&amp;#34;}, &amp;#34;finish_reason&amp;#34;: null}]}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;span class="visually-hidden" role="status" aria-live="polite" data-td-code-status&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;这里的改造思路其实很明确，先解析 GLM 的响应体，再据此拼装成 GPT 的相应格式，然后返回给应用就可以了，然而具体做起来还是有不少坑。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一开始想的是直接 decode 之后分行解析，后来发现不太确定是信道问题还是服务器问题，有的时候接收到的 SSE 事件只有一半（导致 utf-8 decode 失败），或者是两个事件被合并成了一个事件（一个 SSE data 里面有两个 add 事件）。用国内的术语来说这个算粘包？为了解决这个问题，先把行解析改成了正则解析，然后用补充了一个 buffer，如果发现这次的事件不完整就先扔 buffer 里，等下一个事件凑齐了再一起解析。&lt;/li&gt;
&lt;li&gt;改完发现可以正常显示回复了，但是一直不能结束。还需要参考 OpenAI 的响应，额外补充 &lt;code&gt;DONE&lt;/code&gt; 事件。&lt;/li&gt;
&lt;li&gt;这样改完倒是基本能用了，但接下来发现还是不太对劲，生成代码的时候会多一个空格。这里看了响应数据，返回响应的确如此，于是在 data 开头两个空格的时候手动删掉一个。&lt;/li&gt;
&lt;li&gt;然后发现生成 markdown 列表的时候换行消失了。查响应发现有时会有多个 &lt;code&gt;data:&lt;/code&gt;，需要每个都处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目前的效果算是初步可用了吧，但是偶尔如果响应本身不完整（例如某个 SSE 事件返回了不完整的 utf8 编码字符串，下一个事件没有包含丢失的数据），那就会直接报错。不过考虑到实际频率比较低，重试的成本比较小，这里还算可以接受吧。&lt;/p&gt;</description></item><item><title>用 mitmproxy 重定向 OpenAI 请求到 OpenRouter</title><link>https://nekonull.me/share/openrouter-mitmproxy/</link><pubDate>Sun, 22 Oct 2023 20:55:00 +0800</pubDate><guid>https://nekonull.me/share/openrouter-mitmproxy/</guid><description>&lt;h2 id="背景"&gt;背景&lt;a class="td-heading-self-link" href="#%e8%83%8c%e6%99%af" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;最近在尝试使用一些基于 GPT 开发的工具，但遇到了一些网络相关的小问题。因为支付方式的限制，我自己并没有 OpenAI 的账户，实际使用的 API 是其他中间商（aka 二道贩子）转卖而来的， &lt;a href="https://openrouter.ai/docs#models"&gt;OpenRouter&lt;/a&gt; 就是其中一家。（实际上 OpenRouter 做的还更多一些，更像是 LLM 的聚合提供商，除了 OpenAI 也有其他家的 LLM，如 Claude 或是 LLama。）但是很多开源工具并未考虑到这种情况，基本上都是假定用户使用的就是 OpenAI 的官方 API 端点，所以很多时候并不能直接使用各类预先构建好的产物（例如 docker 镜像），而是得把源码 clone 下来，找到 &lt;code&gt;import openai&lt;/code&gt; 或者是类似的调用发起位置，再在附近补充一些参数才能正常使用。手动改代码固然不是不行，但是总归还是有些繁琐，出问题的时候还额外增加了一个需要排查的环节。&lt;/p&gt;
&lt;h2 id="问题"&gt;问题&lt;a class="td-heading-self-link" href="#%e9%97%ae%e9%a2%98" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;有没有更好的，更自动化的方式，例如在网络上加个代理层，在第三方工具无需修改的前提下，就可以将 OpenAI 的请求转换成 OpenRouter 的请求呢？&lt;/p&gt;
&lt;h2 id="解决"&gt;解决&lt;a class="td-heading-self-link" href="#%e8%a7%a3%e5%86%b3" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;那既然都写到这里了，当然是有的。这里的核心是一个 man-in-the-middle （mitm / 中间人）代理，在请求到达代理的时候，修改请求中的内容，使之符合我们的要求，之后再继续对外发送就可以了。&lt;a href="https://mitmproxy.org/"&gt;mitmproxy&lt;/a&gt; 就是这样一个工具。当然它的功能远不止修改请求，在完善的 Python API 的加成下还能做很多其他的事。（同类的工具其他工具，如 Fiddler，应该也能实现，但方法就需要给位自行探索了。）以下就是实现本次需求的核心代码，应该不需要太多解释。&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-3a8f0719-fence-0" data-td-code data-td-code-auto-id
data-td-language="python" data-td-line-count="24"&gt;
&lt;div class="td-code__viewport" id="td-code-3a8f0719-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;mitmproxy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPFlow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 只处理 HOST 为 api.openai.com，且请求体为 JSON 的 POST 请求&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;api.openai.com&amp;#34;&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;POST&amp;#34;&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;content-type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;openrouter.ai&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/api/v1/chat/completions&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;authorization&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bearer sk-xxxxxxxxxx&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;http-referer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;http://localhost:8080/my_great_app&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 应用标识&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;request_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 甚至可以在这里切换模型&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# request_data[&amp;#34;model&amp;#34;] = &amp;#34;anthropic/claude-instant-v1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 需要声明回包支持 stream，否则会等待全部数据到达再返回给应用，无法实现 LLM 打字效果&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;responseheaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;启动 mitmproxy 时需要带上 Python 脚本参数，以及如果有上游代理则需要再声明：&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-3a8f0719-fence-1" data-td-code data-td-code-auto-id
data-td-language="" data-td-line-count="1"&gt;
&lt;div class="td-code__viewport" id="td-code-3a8f0719-fence-1-viewport" data-td-code-viewport&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;mitmweb --mode upstream:http://{upstream_addr} -s openrouter.py&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;启动后会弹出 mitmproxy 的网页控制台，这时候就用第三方工具发请求试试了，一切顺利的话可以看到结果正常返回且网页上显示请求数据。如果出现问题也可以看命令行窗口的输出。如果第三方工具本身支持设置应用内代理（如 &lt;a href="https://github.com/Bin-Huang/chatbox"&gt;Chatbox&lt;/a&gt;）则最理想；不支持的话可以考虑设置系统代理、用 mitmproxy 的&lt;a href="https://docs.mitmproxy.org/stable/howto-transparent/"&gt;透明代理模式&lt;/a&gt;、或者用 &lt;a href="https://www.proxifier.com/"&gt;Proxifer&lt;/a&gt; 这类工具来强制应用代理。&lt;/p&gt;</description></item></channel></rss>