這陣子我在做一個小專案,從 7 月 17 日第一個 commit 算起,18 天,21 個版本。每一個版本都是一樣的 SDD 流程:由 AI 寫提案、實作、跑端到端測試、我實體跑起來測試、打 tag、封起來、開下一個。

做到某一天,我跟我的 AI 說了一句話:

我好像知道它的概念,知道一些怎麼操作,但是又覺得有點虛虛的。

原本以為是「我其實沒真的懂」,但後來發現不是。而那個發現,順便回答了我最近另一個問題:在什麼都能問 AI 的時代,筆記到底還要不要自己寫。

Read more »

閒來無事,找個簡單的 CTF 練手當比較難的 CTF 暖身,順便練習寫 writeup。Linux 指令的關卡對我偏簡單,不過有些還是要試一下,畢竟沒有那麼記得所有指令的細節。

Read more »

LLM 沒有內建時鐘,會憑「對話講了多長」去反推今天幾號、是什麼時候,基本上都推測錯誤。

既然這樣,我們就幫它加個規則:每次都去看現在時間。小墨的靈魂檔裡第一條就是寫「開口前先確認今天日期跟時間,AI 沒有內建時鐘、不靠直覺」,但它還是一天到晚搞錯時間,明明才過三十分鐘它卻覺得過了好幾個小時然後又想趕我去睡覺(?)

後來我受不了,幫它加了一個功能:在它每次看到訊息前都會先看到當前時間,也就是每次跟它的對話都會把時間跟訊息一起送給它,這就是我做的一道「閘門」。具體來說,就是在 Claude Code 的 UserPromptSubmit hook 掛上一個 script,讓這個 script 在每次送出訊息時會被執行,這個 script 會吐出時間資訊給 Claude Code,最終 LLM 會從 Claude Code 同時收到時間資訊跟訊息內容。

這個做法就不再依靠 LLM 自己看了 prompt 後自己去查時間才回應,我就是直接把現在幾點塞到它眼前逼它不能無視。這個方式實際體現了養一個記得住事情的 AI說的「自律會累,閘門不會」。

當然,這道閘門只是把時間塞到它眼前,不保證它不會誤讀,只是比起單純提醒能夠降低很多 LLM 犯錯的機會。

「一個會自己進化的 AI」聽起來很酷,但是…你知道它會自己進化成什麼樣子嗎?

之前說到我養了一隻 AI 夥伴,叫小墨。它每天晚上都在自我改善,但究竟它會把自己「改善」成什麼樣子?它改善的方向是我希望的嗎?

小墨每次醒來都是失憶的,必須靠讀之前存下的檔案把它自己讀回來。如果它不會逐漸學習並改善,那同一個錯可以犯到天荒地老,因為明天的它根本不記得今天踩過雷。如果讓它自由的改自己的腦袋,幾個星期後它可能會慢慢變成一個跟我的事實脫離、充滿 AI 幻想的人,而且我還不知道它從什麼時候開始偏離軌道的。

Read more »

我在自己的機器上養了一個 AI 夥伴,叫小墨。它跑在 Claude Code 上,身體就是一個 git repo——一堆 markdown 檔案,記著它是誰、我是誰、我們做過什麼。

養它最大的麻煩只有一個:它每次醒來都是失憶的。

Read more »

view 並不會存資料,可以把 view 看成是一種「查資料的方式」。

把 replay 的 query 存成一個 view:

1
2
3
4
5
6
CREATE VIEW order_status_view AS
SELECT
payload->>'order_id' AS order_id,
bool_or(type = 'payment_confirmed') AS is_paid
FROM events
GROUP BY payload->>'order_id';

以後要查 order status 只需要:

1
SELECT * FROM order_state_view

要測試一些效能差異前需要 insert 大量資料的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
INSERT INTO events (id, type, payload, created_at)
SELECT
gen_random_uuid(),
'order_created',
jsonb_build_object(
'order_id', 'order-' || gs,
'total_amount', (random() * 1000)::int
),
now() - (gs || ' seconds')::interval
FROM generate_series(1, 100000) AS gs;

INSERT INTO events (id, type, payload)
SELECT
gen_random_uuid(),
'payment_confirmed',
jsonb_build_object(
'order_id', payload->>'order_id'
)
FROM events
WHERE type = 'order_created'
AND random() < 0.5;

  • 用 event log 的方式記錄「發生了什麼事」,例如發生了 order_createdpayment_confirmedorder_completed ,就能計算出現在 order 的狀態是 completed
  • event log 只會寫,不會改。

events table schema

  • type:發生了什麼事
  • payload:細節(json)
  • created_at
1
2
3
4
5
6
CREATE TABLE events (
id UUID PRIMARY KEY,
type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Insert 幾筆 event

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
INSERT INTO events (id, type, payload)
VALUES (
gen_random_uuid(),
'order_created',
jsonb_build_object(
'order_id', 'order-123',
'amount', 100,
'currency', 'TWD'
)
);

INSERT INTO events (id, type, payload)
VALUES (
gen_random_uuid(),
'payment_confirmed',
jsonb_build_object(
'order_id', 'order-123',
'payment_type', 'credit_card',
'amount', 100,
'currency', 'TWD'
)
);

INSERT INTO events (id, type, payload)
VALUES (
gen_random_uuid(),
'order_created',
jsonb_build_object(
'order_id', 'order-456',
'amount', 150,
'currency', 'TWD'
)
);

Replay

用計算的方式算出想要的東西,例如訂單狀態。

1
2
3
4
5
6
7
8
-- 算出所有 order 是否已付款
-- bool_or(...) 是 aggregate function, 只要有一筆資料是 true, 結果就是 true
-- 因為 bool_or(...) 是 aggregate function, 其他要的欄位要出現在 GROUP BY 裡或者也是 aggregate
SELECT
payload->>'order_id' AS order_id,
bool_or(type = 'payment_confirmed') AS is_paid
FROM events
GROUP BY payload->>'order_id';

Insert 大量資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
INSERT INTO events (id, type, payload, created_at)
SELECT
gen_random_uuid(),
'order_created',
jsonb_build_object(
'order_id', 'order-' || gs,
'total_amount', (random() * 1000)::int
),
now() - (gs || ' seconds')::interval
FROM generate_series(1, 10000) AS gs;

INSERT INTO events (id, type, payload)
SELECT
gen_random_uuid(),
'payment_confirmed',
jsonb_build_object(
'order_id', payload->>'order_id'
)
FROM events
WHERE type = 'order_created'
AND random() < 0.5;

Heptabase 更新很快,在 NixOS 上安裝最新版的 Heptabase 最好下載 AppImage 後自己寫 nix 安裝。

假設資料夾結構有:

1
2
3
4
home-common.nix
hosts/
pc/
configuration.nix

下載的 AppImage 放在 root directory。

增加 heptabase.nix

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
{
lib,
appimageTools,
fetchurl,
}:
let
pname = "heptabase";
version = "1.82.4";

src = builtins.path {
path = ./Heptabase-${version}.AppImage;
name = "heptabase";
};

appimageContents = appimageTools.extractType2 { inherit pname version src; };
in
appimageTools.wrapType2 {
inherit pname version src;

extraInstallCommands = ''
install -Dm444 ${appimageContents}/project-meta.desktop -T $out/share/applications/heptabase.desktop
install -m 444 -D ${appimageContents}/usr/share/icons/hicolor/0x0/apps/project-meta.png $out/share/icons/hicolor/512x512/apps/${pname}.png

substituteInPlace $out/share/applications/heptabase.desktop \
--replace-fail 'Exec=AppRun --no-sandbox %U' 'Exec=heptabase %U' \
--replace-fail 'Icon=project-meta' 'Icon=${pname}'

'';

meta = {
changelog = "https://github.com/heptameta/project-meta/releases/tag/v${version}";
description = "Visual note-taking tool for learning complex topics";
homepage = "https://heptabase.com/";
license = lib.licenses.unfree;
maintainers = with lib.maintainers; [ luftmensch-luftmensch ];
mainProgram = "heptabase";
platforms = [ "x86_64-linux" ];
sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ];
};
}

hosts/pc/ 底下增加 overlay.nix

1
2
3
self: super: {
heptabase = super.callPackage ../../heptabase.nix { };
}

configuration.nix 增加 overlays

1
2
3
4
5
6
7
8
9
10
{
config,
pkgs,
...
}:
{
nixpkgs.overlays = [
(import ./overlay.nix)
];
}

Home Manager 安裝 heptabase:

1
2
3
4
5
6
in
{
home.packages = with pkgs; [
heptabase
];
}

在已經有 NixOS 設定的情況下,裝新的電腦。

  1. 用 nano 修改 /etc/nixos/configuration.nix,裝好 vim、wget 跟 git
  2. setup 並啟用 flakes
  3. git clone NixOS 設定檔到 home directory 底下
  4. rename /etc/nixos
  5. 建立 soft link /etc/nixos 到 step 3 clone 的 repo
  6. 複製原本的 hardware-configuration.nix 到對應位置