iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0

前言

書目接上之後,我拿兩篇論文丟進去編一次。進度列一路出現「更新」,旁邊跟著路徑。編完打開那幾頁,有的根本沒建出來。

我以為把工具名稱跟路徑列出來,工作紀錄就夠用了。

進度列當初只畫呼叫

工作紀錄把一次工具拆成兩筆:先寫 tool,等回傳再寫 result。我把 result 濾掉了,覺得那是噪音。拒絕不會讓整份工作停掉。makeExec 把 NoteError 收成一段 JSON,裡面有一個 error 字串,迴圈接著跑下一步。進度列只看到 update_note,就畫「更新」加路徑。那個畫面跟實際寫進去是兩回事。

export function agentSteps(log: AgentEvent[]): AgentStep[] {
  const steps: AgentStep[] = [];
  for (const e of log) {
    if (e.type === 'tool') { steps.push({ tool: e.tool ?? '', input: e.input as Record<string, unknown> | undefined, failed: false }); continue; }
    if (e.type !== 'result') continue;
    const last = steps[steps.length - 1];
    if (!last || last.error) continue;
    try {
      const out = JSON.parse(e.output ?? '');
      if (out && typeof out === 'object' && typeof out.error === 'string') { last.failed = true; last.error = String(out.message ?? out.error); }
    } catch { /* not JSON, or truncated at 400 chars: treat as success, which is what it looks like */ }
  }
  return steps;
}

出處:src/ingest.ts:117-130

現在讀的時候兩筆要對上。回傳裡有 error,那一步才標失敗,前面換成 ✖,後面帶上原因。對話寫回 session 走後端這支。進度列走前端一支一樣的判斷,寫在 web/src/lib/agentLog.ts。

回傳寫進工作紀錄時只留前四百個字,見 src/ai/providers.ts:71。剖不開就還是當成成功。目前的解法很土,但夠用。

對話也靠同一份紀錄。agent 有時把答案寫進 wiki/queries/ 就停了,對話框是空的。你看著空泡泡,會以為沒做。做完之後用 writtenPaths 列出實際寫進去的頁,只收成功的 create_note 與 update_note。有寫進哪些頁,就列出路徑。沒回覆也沒寫入,才講這句。

export function writtenPaths(log: AgentEvent[]): string[] {
  const seen = new Set<string>();
  for (const s of agentSteps(log)) {
    if (s.failed || (s.tool !== 'create_note' && s.tool !== 'update_note')) continue;
    if (typeof s.input?.path === 'string') seen.add(s.input.path);
  }
  return [...seen];
}

出處:src/ingest.ts:133-140

const wrote = writtenPaths(log);
const lang = await workspaceLang(ws);
const fallback = wrote.length
  ? pick({ 'zh-TW': `(agent 沒有回覆文字,但把結果寫進了這些頁)\n${wrote.map(p => `- ${p}`).join('\n')}`,
           en: `(The agent wrote no reply, but it did write these pages.)\n${wrote.map(p => `- ${p}`).join('\n')}` }, lang)
  : pick({ 'zh-TW': '(agent 沒有回覆文字,也沒有寫入任何頁)', en: '(The agent wrote no reply and changed nothing.)' }, lang);
const reply: ChatMessage = { role: 'assistant', content: finalText || fallback, at: new Date().toISOString(), jobId: job.id, steps: done.steps, tokens_in: done.tokens_in, tokens_out: done.tokens_out, cost_usd: done.cost_usd, tools };

出處:src/chat.ts:102-108

NOT_FOUND 少寫了下一步

agent 常先叫 update_note。頁不存在就回 NOT_FOUND。之前那則訊息只寫「找不到筆記」,它把這步丟掉,進度列照樣畫「更新」。你以為那一頁被改過,其實沒有。

之前我們已經把 create_note 碰到已存在寫成「請改用 update_note」。找不到頁該改呼叫 create_note,我漏了。

if (!cur.rows[0]) throw new NoteError('NOT_FOUND', {
  'zh-TW': `找不到筆記:${path}。這一頁還不存在——要新建請改用 create_note(網頁請用「+ 新增」)。`,
  en: `Note not found: ${path}. This page does not exist yet — use create_note to make it (in the web app, "+ New").`,
});

出處:src/notes.ts:281-284

編纂提示詞跟各份模版的規則頁也補了:新頁用 create_note,既有的頁才用 update_note。收到 NOT_FOUND 就改新建。

整句英文搜不到中文標題

之前我們已經講過,搜尋是 ILIKE。當時整句當一個字串去比。論文是英文,概念頁是中文,整句對不上標題。內文就算有相關的詞,拆在兩句裡也搜不到。回傳零筆,它就另開一頁。同一個概念變成兩頁,之後的引用就散了。

export function searchTerms(query: string): string[] {
  return (query.match(/"[^"]+"|\S+/g) ?? [])
    .map(t => t.replace(/^"|"$/g, '').trim())
    .filter(Boolean)
    .slice(0, 8);
}

出處:src/notes.ts:112-117

每個詞各做一次 ILIKE,一頁要同時含這些詞才算命中。加引號的片段維持整句。中文沒有空白,整段查詢仍是一個詞,跟以前一樣。

零筆時工具多帶一句:這不代表沒有相關頁,先讀 wiki/index.md。概念頁的 front-matter 加上 aliases:,寫另一個語言的常見說法。搜尋比的是標題與 content_md,front-matter 就在 content_md 裡。之後換語言的來源才找得到這一頁。

小結

我第一版把回傳濾掉。拒絕是 JSON,工作繼續跑,進度列只看到「更新」加路徑。


上一篇
Day 16 - 書目與學術引用
下一篇
Day 18 - 用雜湊比對分出規則頁改過沒有
系列文
為你自己蓋一座會複利的知識庫——WikiBrain 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言