AI相关的探索经验

vLLM 与 bge-m3

huggingface上的 bge-m3 是 pytorch 模型,而 vLLM 喜欢 safetensors。好在讨论区已经有自动转换bot给出了pr:Adding safetensors variant of this model。那么用huggingface-cli下载到本地,通过指定revision的方式就可以了:

1
huggingface-cli download BAAI/bge-m3 --revision refs/pr/116 --exclude pytorch_model.bin --local-dir ./bge-m3

而 vLLM serve 的命令是:

1
2
3
4
5
6
7
VLLM_ATTENTION_BACKEND=FLASH_ATTN VLLM_USE_V1=0 vllm serve bge-m3 --max-num-seqs 2 \
--enforce-eager \
--task embed \
--disable-log-requests \
--swap-space 0 \
--gpu-memory-utilization 0.2 \
--served-model-name bge-m3

Read More

搬迁至hexo的记录

punycode deprecated 及泛用的 override

首先用Stackoverflow - chenop: trace deprecated的方法找到是谁在用deprecated的:

1
npx cross-env NODE_OPTIONS=--trace-deprecation npx hexo g

然后在package.json中用overrides来强行更新dependency的dependency:

1
2
3
4
5
6
7
8
9
10
{
//...
"overrides": {
"markdown-it": "^14.1.0",
"warehouse": "^6.0.0",
"glob": "^11.0.1",
"stylus": "^0.64.0"
},
//...
}

这个方法来自于:Stackoverflow - Choco Li

最后还有隐藏的deprecated,在package-lock.json中搜索depreca就能看到了。

mermaid tag

threeq/hexo-tag-mermaid 用的是提前render好的方式,但是需要 build 时就调用 mermaid,带来了很多复杂性。在chatgpt帮助下,我改成了引入 mermaid.js,让客户端渲染的方案sieveLau/hexo-tag-mermaid。格式:

1
2
3
{% mermaid %}
....
{% endmermaid %}

Read More

fix oc_ext_apps corrupted

在 upgrade nextcloud 的过程里,执行 occ upgrade 的时候发现错误提示:

1
- ERROR: An exception occurred while executing a query: SQLSTATE[HY000]: General error: 1877 Table `nextcloud`.`oc_ex_apps` is corrupted. Please drop the table and recreate.

搜索了 nextcloud 社区,虽然没有直接的解决方法,但是从这个对话里得到了 schema。虽然是 postgresql 的,但是让 chatgpt 转换成 mariadb 就行了。修复过程如下:

  1. 连接到 mariadb,root 身份直接 mariadb -uroot
  2. 然后在 mysql shell 中执行以下命令(这里数据库名字之类的用的默认名字):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
USE nextcloud;
DROP TABLE oc_ex_apps;
CREATE TABLE nextcloud.oc_ex_apps (
id BIGINT NOT NULL,
appid VARCHAR(32) NOT NULL,
version VARCHAR(32) NOT NULL,
name VARCHAR(64) NOT NULL,
daemon_config_name VARCHAR(64) NOT NULL DEFAULT '0',
port SMALLINT NOT NULL,
secret VARCHAR(256) NOT NULL,
status JSON NOT NULL,
enabled SMALLINT NOT NULL DEFAULT 0,
created_time BIGINT NOT NULL
);

这样就修好了。不过这个 schema 是 30.0.x 的,为了避免刻舟求剑,可以定时用以下命令导出整个 nextcloud 数据库中所有 table 的 schema:

1
mariadb-dump --no-data nextcloud > nextcloud_schema.sql

Read More

Auto Suspend and Wake On LAN

自从购买了 4070 Ti Super 之后 AI 机就处于长期启动的状态。但是仔细思考平时的使用情况,实际上不是24小时不间断需要AI辅助。根据父亲对他自己的电脑的测量,待机状态下都会有30多瓦的功耗,这是不必要的。于是在参考Wiki、结合之前在windows上的 wake-on-lan 的经验,我决定在 AI 机上做一个 autosuspend,同时也要有办法在请求来的时候唤醒并处理。

具体分成两部分:AI 机在没有活跃请求连接ollama端口的时候等待5分钟,然后睡眠;网关机如果发现 AI 机昏迷了导致 502 错误,就会执行 wol 来唤醒。

autosuspend

自动昏迷采用的是 Autosuspend,Arch Linux用 AUR 安装即可。安装完成之后修改 /etc/autosuspend.conf

  1. idle_time 设置为 300(5分钟);
  2. 下面一大堆预设的 check 全部注释,我们只使用 ActiveConnection:
1
2
3
4
[check.AI]
class = ActiveConnection
enabled = true
ports = 11434

如果有需要把22端口也加上防止ssh连接时意外昏迷。然后 systemctl enable --now autosuspend启动服务即可。

wake-on-lan

这部分有点复杂,因为 AI 机的板载网卡是 r8125,会有几个问题:

  1. windows的驱动会在关机时禁用wake-on-lan,影响其他系统使用这个功能;这个可以通过在 UEFI 里面打开 PXE (一般在Network Stack里面)来让 UEFI 无视windows驱动的设置
  2. Arch Linux 的内核用 r8169 模块来驱动 r8125,但是会导致 wake-on-lan 无法工作,需要从 AUR 安装 r8125-dkms,并且屏蔽 r8169 的加载。具体参考 Arch Wiki

Read More

Open WebUI with Qwen QwQ on Tavily Search

Open WebUI虽然内置了 Web Search 功能,而且能够调用 Tavily,但是效果比较一般。在社区提供的 Tool 中,victor1203提供了一个可用的 Tavily 搜索工具,Web Search with Tavily. 不过现时 Tavily 的 API 有所变动,所以需要进行一些修改。

我的 Open WebUI 是使用 docker 部署的,为了能够使用 tavily-python,需要进入 docker 容器并安装相应的依赖。步骤:

  1. 进入 docker 容器:docker exec -it openwebui bash
  2. 安装依赖:d open_webui && pip install tavily-python
  3. 退出 docker 容器:exit
  4. 重启 docker 容器:docker restart openwebui

然后打开“工作空间”,点击 “工具”,新建一个工具,填入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""
title: Tavily Search Tool
author: victor1203
maintainer: sievelau
description: This tool performs internet searches using the Tavily API to get real-time information with advanced context and Q&A capabilities
required_open_webui_version: 0.4.0
requirements: tavily-python
version: 1.1.0
licence: MIT
"""

from pydantic import BaseModel, Field
from typing import Optional, Literal
from tavily import TavilyClient


class Tools:
def __init__(self):
"""Initialize the Tool with valves."""
self.valves = self.Valves()
self._client = None

class Valves(BaseModel):
tavily_api_key: str = Field(
"", description="Your Tavily API key (starts with 'tvly-')"
)
search_depth: str = Field(
"basic", description="Search depth - basic or advanced"
)
include_answer: bool = Field(
True, description="Include an AI-generated answer in the response"
)
max_results: int = Field(
5, description="Maximum number of search results to return (1-10)"
)

def _get_client(self) -> TavilyClient:
"""Get or create Tavily client instance."""
if self._client is None:
self._client = TavilyClient(api_key=self.valves.tavily_api_key)
return self._client

async def search(self, query: str, __event_emitter__=None) -> str:
"""
Perform a Tavily search and return the results.

Args:
query: The search query string
search_type: Type of search to perform:
- regular: Standard search with full results
- context: Optimized for RAG applications
- qa: Quick answer to a specific question

Returns:
A formatted string containing the search results
"""
try:
# Input validation
if not self.valves.tavily_api_key:
return "Error: Tavily API key not configured. Please set up the API key in the tool settings."

# Emit status that search is starting
if __event_emitter__:
await __event_emitter__(
{
"type": "status",
"data": {
"description": f"Initiating Tavily search...",
"done": False,
},
}
)

client = self._get_client()

# Perform the search based on type
if __event_emitter__:
await __event_emitter__(
{
"type": "status",
"data": {
"description": "Fetching search results...",
"done": False,
},
}
)
result = client.search(
query=query,
search_depth=self.valves.search_depth,
include_answer=self.valves.include_answer,
max_results=self.valves.max_results,
)

# Format regular search results
formatted_results = ""

# Include AI answer if available
if self.valves.include_answer and "answer" in result:
formatted_results += f"AI Answer:\n{result['answer']}\n\n"

# Add search results
formatted_results += "Tavily Search Results:\n\n"
for i, item in enumerate(
result.get("results", [])[: self.valves.max_results], 1
):
formatted_results += f"{i}. {item.get('title', 'No title')}\n"
formatted_results += f"URL: {item.get('url', 'No URL')}\n"
formatted_results += f"Content: {item.get('content', 'No content')}\n"
formatted_results += f"Relevance Score: {item.get('score', '0.0')}\n"
formatted_results += "\n"

if __event_emitter__:
await __event_emitter__(
{
"type": "status",
"data": {
"description": "Search completed successfully",
"done": True,
},
}
)

return formatted_results

except Exception as e:
error_message = f"An error occurred while performing the search: {str(e)}"
if __event_emitter__:
await __event_emitter__(
{
"type": "status",
"data": {"description": error_message, "done": True},
}
)
return error_message

具体的修改是:

  1. 删除了 search type,因为现在只有 search,具体的类型通过 topic 来指定,但是作为 genereal search,我们不需要这个参数。
  2. 把 formatted_results 的引导内容添加上 Tavily 这个关键词,方便 RAG Template 中指导模型
  3. 删除了 snippet,转而增加了 content 来指定搜索结果的内容
  4. 增加了 score,方便后面更改 RAG Template 来告诉模型根据此字段来判断相关度

之后,在工作空间的“模型”中新建一个 WebSearch,工具勾上我们刚才新增的这个tool,这就做好了一个 agent。

但是我们还需要更改 RAG Template。默认的 template 是设计成与内置的 web search 进行配合的,但是我们用 tool 来传递搜索结果会变成只有一个 source id,导致引用不正确。所以需要在 guideline 增加两条:

  1. - If the context contains "Tavily Search Results", you should not include inline citation. You should indicate the source url in this syntax: [Title](URL). Make sure you are using markdown link syntax and take care of the quotes. This rule take precedence over the previous rule about source_id.
  2. - If the context contains "Tavily Search Results", you should determine result quality by the Relevance Score.

Read More

NFS on ZFS and MacOS

之前通过 SMB 共享的一个目录,今天在写入 3G 左右大小的文件的时候,复制结束之后报错 -51。由于搜索了一番仍未找到解决方案,只能退而求其次试用 NFS 共享。根据 Archwiki 上关于 NFSZFS 的配置,结合今天的试错经历,总结如下:

配置固定端口方便 firewall 放行

需要配置 rpc.mountd 和 lockd 的端口,这两个的配置方式有所不同。

对于 rpc.mountd,只需要修改 /etc/nfs.conf,找到 [mountd] 这一节,去掉 port=0 这一行的注释并修改成自己喜欢的端口比如 2050 即可,然后 restart nfs-server.service 即可。

对于 lockd,亦即 rpcinfo -p 中输出的 nlockmgr 条目,则需要新建 /etc/modprobe.d/nfs.conf,填入以下内容来设置端口,比如改成 2051:

1
options lockd nlm_udpport=2051 nlm_tcpport=2051

保存之后需要 reboot 才能生效。

综合以上配置,加上 nfs 自身使用的 111 端口和 nfs v4 的 2049 端口,nftables需要以下规则放行相关端口:

1
2
tcp dport {2049,2050,2051,111} accept comment "nfs"
udp dport {2050,2051,111} accept comment "nfs"

如果不放行 mountd 的端口,那么在 Finder 连接 NFS 时会直接显示无法连接;如果不放行 lockd 的端口,那么 Finder 能够正常连接,但是打开子目录的时候就会超时并且断开连接。

Read More

Finance::Quote on MacOS and Windows

MacOS 上的 Finance::Quote 由于 Date::Simple 的 test 不通过会导致无法正常安装:

1
2
3
t/date.t ...... 1/227 
# Failed test at t/date.t line 106.
# Looks like you failed 1 test of 227.

然而强制安装是可以正常运行的,所以使用以下命令就能强制安装了:

1
sudo env ARCHFLAGS='-arch arm64 -arch arm64e -arch x86_64' cpan -f -i Finance::Quote

Windows 稍有不同,首先安装 Strawberry Perl,然后打开 cmd,用 cpan -T 来跳过测试、强行安装需要的包。“需要的包”可以查看 gnucash 安装目录的 gnc-fq-update 文件,目前包括:

  • Test2
  • Finance::Quote
  • JSON::Parse

Read More

AFM Chapter 16

Announcement: All pictures are from the book Jonathan Berk, Peter DeMarzo - Corporate Finance (2024).

MM理论讨论了 tax shield 之后,变成了

给出的 optimal capital structure 就是 interest 等于 earning before tax,这样就可以完全不交税、实现 investor 收益最大化。然而现实世界里面的公司不会这样做,甚至负债水平远低于 optimal leverage。这一章讨论的就是还有什么其他的因素影响了公司选择 capital structure。


在 perfect capital market 下,if a firm:

  1. has access to capital markets
  2. can issue new securities at a fair price

那么只要 assets exceeds liabilities 就不会 default。


Decline in value is not caused by bankruptcy. 项目成败决定是否会出现 economic distress;leverage 影响是否会有 financial distress.

Read More

OpenGFW Experience

因为现在很多 App 会内置 resolver,只会跟系统的 DNS 请求一个它的 HTTPDNS 服务器的地址,后续就不会请求系统的 DNS 了,导致 DNS 广告过滤会失效。不想24小时开着 Surge,但是又想制裁广告的话,就只能用类似 SNI 阻断的方式了。

很久之前就听闻过 OpenGFW 的大名,这次终于找到了使用它的场景。恰好 AUR 里面有人已经做好了 PKGBUILD,直接一键安装就行。但是 service 文件得自己写,简单写一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Unit]
Description=OpenGFW
After=network.target nftables.service

[Service]
Type=simple
WorkingDirectory=/run/OpenGFW
RuntimeDirectory=OpenGFW
ExecStart=/usr/bin/OpenGFW -c /etc/OpenGFW/config.yaml /etc/OpenGFW/rules.yaml
ExecReload=/bin/kill -HUP $MAINPID
TimeoutStopSec=5s
PrivateTmp=true
ProtectSystem=strict
NoNewPrivileges=true
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target

最简单的屏蔽就是使用 geosite:category-ads-all 来确定哪些是广告域名了。我们针对 HTTP、TLS 以及最潮最 IN 的 QUIC 进行判断,可以写以下规则到 rules.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- name: block tls ad geosite
action: block
log: true
expr: geosite(string(tls?.req?.sni), "category-ads-all")

- name: block http ad geosite
action: block
log: true
expr: geosite(string(http?.req?.headers?.host), "category-ads-all")

- name: block quic ad geosite
action: block
log: true
expr: geosite(string(quic?.req?.sni), "category-ads-all")

如此一来就可以 start/enable 了。试过确实好,不过不建议对路由器本机生效,也就是 config.yamllocal 要设置成 false我对 0.4.1 版本进行的 speedtest 测试发现,对本机生效的话,延迟从 1 ms 提高到了 11 ms,下载和上传从 900 Mbps 分别降低到了 800 Mbps 和 600 Mbps,损失很大。

Read More

Quick Walk in Sai Kung

西贡去一趟不容易,从家里接驳去西贡专线的小巴不好坐,下次还是坐观塘线过去彩虹再转车比较好。

不得不说这个地方的海真的漂亮,不加滤镜就能很好看。40块港币来回船票,在香港算是便宜了。

我们今天只爬到黑山顶就折返了,主要是赶着回家看书。下次比较空闲的时候再来一趟,从厦门湾爬到桥咀,走个全程。

塑料虾诱饵

黑山顶

可爱暴龙

蓝海

Read More