lazyBoy/docs/agent-experience.md

108 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AI 使用體驗
[← 回到 README](../README.zh-TW.md)
這份文件記錄 LazyBoy 怎麼讓「AI 用電腦」這件事變得跟人一樣順,以及背後的取捨。
三個主題其實是同一件事:系統如果把 AI 使用電腦當成一系列無狀態的請求AI 就必須每輪重新
交代自己在哪裡、在做什麼,界面也只能用輪詢去猜現在發生了什麼。
| 主題 | 一句話 | 細節 |
| --- | --- | --- |
| 任務長度 | 沒有輪數額度,只有偵測鬼打牆的保險絲 | [輪次政策](./operations.md#任務跑多久輪次政策) |
| 容器內終端機 | 一台活的 tmux人和 AI 看同一個 shell | [終端機](./operations.md#容器內終端機) |
| 聊天即時性 | 事件推送,輪詢只是兜底 | 本文件下面 |
## 輪數不是額度,是偵錯工具
以前一個 run 固定 40 輪,等於假設所有任務一樣長:洗資料這種事情做不完,但也不會因此變成
錯誤,只是被腰斬。現在的正常結局是**把事情做完**(模型提出驗證,或明白說它卡在哪裡),會
被停下來的只有鬼打牆:同一個會改變狀態的動作連做六次、同一個錯誤連錯八次、連續十四個動作
全部失敗。觀察、等待、輪詢不算重複動作,所以等一個長 build、等下載、輪詢佇列都不會被打斷。
設計上要記住兩件事:
- 提示(「你已經做同一件事三次了」)永远不會停掉任務,只有重複到像故障才會暫停,暫停也是停在
現況、按「繼續」接下去,不重來。
- 保險絲(`budget_exhausted`)代表迴圈失控,是故障不是成績;它照樣可以續跑,但值得去看執行記錄。
純聊天仍然是 4 輪上限:那是避免模型在閒聊裡燒額度,跟任務長度無關。
## 終端機是一台活的 tmux
`shell` 工具以前每條命令都是一次性的 `bash -lc``cd` 留不住、`export` 留不住、背景起的服務
跟著一起死,模型每次都要重新走回工作目錄,遇到互動式程式就整個卡死。現在每個終端機名字對應
容器內一個 tmux session`lazyboy-main`AI 用的就是人在桌面上會用的那個 shell。
有了持久終端機,模型才做得順這些事:
- `python -m venv .venv && source .venv/bin/activate` 之後,下一條命令還在同一個環境裡。
- `npm run dev &` 之後可以繼續做別的,回來用 `log_lines` 讀同一個終端機的輸出。
- `ssh`、`gdb`、`psql`、Python REPL 這類會問你話的程式,用 `keys` 回它,而不是直接超時。
- 按錯 Ctrl-C 只會中斷那台終端機裡正在跑的東西,不會把整個工作環境帶走。
**完成是怎麼判定的。** API 進到容器是一次性的 execargv 進、stdout 出,沒有串流),所以
「跑完了沒」不是看 exec 有沒有結束,而是看終端機裡印了什麼:每次呼叫會在 shell 內 source 一個
檔名隨機的腳本,腳本用 `trap ... EXIT` 印出 `LB_END <nonce> rc=<code>`;還看到 `LB_READY`
代表上一件事已經被中斷、終端機是可用的。標記只在**行首**比對——命令本身會被 shell echo 一次,
那行也帶有標記,比錯就會以為已經跑完了。
**死了會自己站起來。** `exit`、`exec bash`、被 kill 掉都會讓 pane 不見;這時候終端機重新
`respawn`,並把剛才的工作目錄與 `export` 過的環境變數還原回去,模型不需要重新交代。
人可以隨時看同一台終端機,或直接接手(指令見
[容器內終端機](./operations.md#容器內終端機)。這不隻是好看AI 卡在同一個地方時,你看到的
就是它看到的那個 shell。
行為用 `python3 tests/shell-session.test.py` 驗證10 個案例,含 exit code 還原、自我重生、
中斷後可續用)。
## 聊天是推送,不是輪詢
事件早就寫進 `events` 表,也有 SSE 端點,但兩端都在猜:伺服器每 750 ms 掃一次資料庫,瀏覽器
每 2 秒重刷一次全部狀態(訊息、電腦狀態、螢幕、技能、心跳一起打)。所以一個回覆最快也要兩輪
輪詢才被看見。
現在的管線:
1. 寫事件的事實不變——`events` 表仍然是順序與重放的依據(游標是 `threads.next_event_seq`
斷線重連用 `Last-Event-ID` 從資料庫重放,不會漏也不會重複)。
2. 提交之後往進程內的 wake channel 敲一下(`WakeBus`。SSE 端點不再固定間隔輪詢,而是等敲;
敲不到時(多副本、讀者落後)以 5 秒兜底輪詢補上。
3. 瀏覽器用 `EventSource` 訂閱 `/api/sessions/{id}/events`,事件在 120 ms 的窗口裡合流,
一回合變動只刷新一次。
實測(本機 loopback、丟棄式資料庫、同一台機器
| 事件 | 以前 | 現在 |
| --- | --- | --- |
| 自己發的訊息出現在畫面 | 最多 2 秒 | 送出去後約 25 ms |
| 「思考中」指示出現 | 最多 2 秒 | 約 32 msworker 也被敲醒,不再等 200 ms 定時器) |
| 模型回覆出現 | 寫入後最多 2.75 秒 | 寫入後約 25 ms模型本身要幾秒是另一回事 |
順帶把浪費掉的工作拿掉:分頁藏在後面時不再刷新(回到前景立刻補一次),心跳從 2 秒改成
60 秒——一次心跳買的是 15 分鐘控制租約、避免 10 分鐘閒置暫停60 秒是很寬的餘裕。
**壞了會怎樣。** SSE 被 proxy 擋住或斷線,瀏覽器察覺後自動回到 2 秒輪詢,等於退回以前的行為,
不會變成不更新伺服器端事件順序仍然只以資料庫為準wake 不見得可靠,只是慢。
自己量一次:
```bash
curl -N -b cookies.txt http://127.0.0.1:3101/api/sessions/<id>/events
# 另開一個終端機送訊息,看事件幾毫秒後出現在這條串流裡
```
## 已知取捨
- 模型回覆還是「整個完成」才出現,沒有串流 token。思考中的狀態有顯示但要像 ChatGPT 那樣逐字
冒出,需要把 completion 換成串流並處理中斷/接續,這是下一件事。
- 電腦狀態(執行中/暫停、誰在控制)沒有自己的事件,只能靠兜底輪詢更新,`LIVE_POLL_MS` 目前是 4 秒;
桌面畫面本身是即時串流,只有狀態列會慢幾秒。要真正事件化得把 `computers` 的狀態變更也寫進
`events`(狀態是 bot 層、事件是執行緒層,要先決定寫到哪條執行緒)。
- 滑鼠停在頭像上看到的執行記錄泡泡,開啟時仍是輪詢(它只在滑鼠停留時開啟,量很小)。
- wake channel 是單進程的。多副本部署時,其他副本靠 5 秒安全輪詢追上;真要横向擴充應改用
Postgres `LISTEN/NOTIFY`
- 持久終端機需要桌面映像檔裡的 `tmux`;舊映像檔會退回一次性 shell`docker compose build computer`
之後就有了。
- 容器執行檔仍以 uid 1000、限制環境變數的方式進入容器持久化的是 shell 狀態,不是憑證。