---
title: "Nhật ký cho agent: ghi gì để lần sau điều tra được"
url: https://www.stacklogs.io.vn/ky-thuat/nhat-ky-cho-agent/
language: vi
author: "Nhu Duy Thin (Thinnd)"
published: 2026-10-31
updated: 2026-10-31
section: "Kỹ thuật phần mềm"
tags: ["nhật ký", "AI agent", "giám sát", "JSON Lines", "điều tra sự cố", "danh sách kiểm"]
summary: "Hãy để một lớp nằm ngoài agent ghi lại mỗi hành động thành một dòng JSON: thời gian, ai làm, công cụ hoặc lệnh, tham số đã che khóa bí mật, mã kết thúc, tệp bị đổi và yêu cầu gốc. Chỉ cho phép thêm dòng, không cho agent sửa. Khi có sự cố, truy vấn dòng đó để dựng dòng thời gian, tìm lỗi đầu tiên và liệt kê tệp đã đổi."
license: "Cite with the author name and the canonical URL above."
---

# Nhật ký cho agent: ghi gì để lần sau điều tra được

> Dành cho bất kỳ ai giao việc cho AI agent, kể cả người mới. Không cần biết gì về quan sát hệ thống hay bảo mật. Thuật ngữ nào cũng đi kèm một ví dụ đời thường, và phần code được đánh dấu để bỏ qua nếu bạn chưa cần.
>
> Số liệu lấy từ nguồn công khai, có liệt kê cuối bài. Ví dụ ghi là "minh họa" được dựng lại, không nhắc đến người hay công ty cụ thể. Phần ví dụ chạy thật là một mô phỏng do tôi viết bằng Python, không phải nhật ký của một công cụ thương mại. Cách AI quyết định hành động bên trong chưa ai hiểu hết, nên các hình dung dưới đây chỉ là cách tưởng tượng để dễ nhớ.

---

## Tóm tắt nhanh

- Khi bạn giao việc cho agent rồi đi họp, bạn không nhìn thấy từng bước nó làm. Nếu không có dấu vết, lúc có chuyện chỉ còn cách đọc lại cả cuộc trò chuyện dài để đoán.
- **Nhật ký hành động** là một dòng ghi cho mỗi việc agent làm: lúc nào, công cụ hay lệnh gì, kết quả ra sao, tệp nào bị đổi. Mỗi dòng một bản ghi JSON (định dạng JSON Lines) thì tìm kiếm được bằng vài dòng code hoặc lệnh `jq`.
- **Ghi vừa đủ.** Không ghi khóa bí mật, dữ liệu cá nhân hay toàn bộ nội dung tệp. Phải che trước khi ghi, vì nhật ký lộ khóa cũng nguy hiểm như code lộ khóa.
- **Lớp ghi nên nằm ngoài agent** và chỉ cho thêm dòng. Nhật ký mà chính agent sửa được thì không còn là bằng chứng.
- Trong ví dụ chạy thật của bài, 25 hành động tạo ra tệp 7.170 byte. Ba câu hỏi điều tra (dòng thời gian, lỗi đầu tiên, tệp đã đổi) có đáp án ngay khi chạy script, và một dòng bị sửa bị phát hiện.
- Nhật ký cho biết **điều gì đã xảy ra**, không cho biết **điều đó có đúng không**. Nó bổ sung chứ không thay cho việc đòi bằng chứng và kiểm thử.

---

## Sau bài này bạn giải thích được

1. Vì sao một phiên agent không ai xem vẫn cần để lại dấu vết, và dấu vết khác gì cuộc trò chuyện đã lưu.
2. Mỗi dòng nhật ký nên có những trường nào, và những gì tuyệt đối không được ghi.
3. Cách dùng nhật ký sau sự cố: dựng dòng thời gian, tìm lỗi đầu tiên, liệt kê tệp bị đổi, và phát hiện dòng bị sửa.

---

## Từ khó, nói dễ

| Thuật ngữ | Nói dễ hiểu |
|---|---|
| **Audit trail** (nhật ký hành động, dấu vết kiểm tra) | Cuốn sổ ghi lại từng việc đã làm, theo thứ tự thời gian, như sổ biên nhận của cửa hàng. |
| **Log** (bản ghi) | Một dòng trong cuốn sổ đó. |
| **JSON Lines** | Mỗi dòng trong tệp là một bản ghi hoàn chỉnh. Thêm bản ghi chỉ là thêm một dòng cuối tệp. |
| **Redaction** (che thông tin nhạy cảm) | Gạch đen phần bí mật trước khi viết vào sổ, như che số thẻ trên biên lai. |
| **Hook** (móc) | Chỗ công cụ cho bạn gắn một đoạn lệnh chạy mỗi khi có một sự kiện, ví dụ ngay sau khi agent chạy một lệnh. |
| **Timeline** (dòng thời gian) | Danh sách việc xảy ra theo thứ tự, để thấy việc nào dẫn đến việc nào. |
| **Append-only** (chỉ thêm) | Cuốn sổ chỉ cho viết thêm xuống cuối, không cho tẩy xóa trang cũ. |
| **Hash chain** (chuỗi băm) | Mỗi dòng ghi kèm một "dấu vân tay" của dòng trước. Sửa một dòng thì dấu vân tay các dòng sau không còn khớp. |

---

## 1. Hình dung trước: camera cửa hàng và sổ biên nhận

Hình dung bạn mở cửa hàng và thuê một nhân viên rất nhanh tay. Hôm nay bạn đi vắng. Chiều về, két tiền lệch 200 nghìn. Nếu cửa hàng không có gì ghi lại, bạn chỉ còn cách hỏi nhân viên, và nhân viên có thể trả lời rất thật thà mà vẫn nhớ sai.

Giờ hình dung cửa hàng có hai thứ. Một camera quay lại những gì diễn ra ở quầy. Một cuốn sổ biên nhận ghi từng giao dịch: mấy giờ, bán gì, thu bao nhiêu. Bạn không cần xem cả ngày quay phim. Bạn mở sổ, tìm giao dịch lúc 3 giờ chiều, thấy ngay chỗ lệch. Máy bay cũng vậy: hộp đen ghi lại thao tác để sau tai nạn người ta dựng lại chuyện gì đã xảy ra, dù phi công không còn để hỏi.

Agent làm việc giống nhân viên nhanh tay đó: nó chạy hàng chục lệnh trong vài phút. Cuộc trò chuyện là lời nó kể. **Nhật ký hành động là cuốn sổ biên nhận**: nó ghi cái đã làm chứ không phải cái đã nói.

**Điều rút ra:** nhật ký không để bắt lỗi nhân viên. Nó để khi có chuyện, bạn tìm ra nguyên nhân trong vài phút thay vì vài giờ đoán mò.

---

## 2. Vì sao phiên không ai xem lại cần dấu vết

Càng giao việc cho agent, bạn càng ít xem từng bước. Trang giới thiệu báo cáo xu hướng lập trình agent 2026 của Anthropic nói các lãnh đạo kỹ thuật đang cân bằng giữa năng suất với giám sát, chất lượng và bảo mật, và liệt kê chủ đề điều phối nhiều agent. Tôi chỉ đọc được trang giới thiệu, còn bản đầy đủ nằm sau một biểu mẫu đăng ký nên **tôi chưa đọc phần thân báo cáo** và không trích thêm điều gì từ đó.

Một số liệu khác: khảo sát Stack Overflow 2026 ghi 69,9% người trả lời hỏi một AI agent khi gặp việc lạ (con số tôi đọc qua bản tóm tắt, chưa đối chiếu từng bảng). Nghĩa là agent đã thành một phần việc hằng ngày, và phần việc đó thường diễn ra khi bạn không nhìn.

Khi đó có hai kiểu mất mát. Kiểu một: có sự cố mà không ai biết agent đã chạy lệnh nào, nên điều tra chậm. Kiểu hai: không có sự cố nhưng đồng nghiệp hỏi "sao tệp test bị đổi?" và bạn phải đọc lại cả hội thoại dài để trả lời. Bài [Làm cho kết quả của AI tự chứng minh](https://www.stacklogs.io.vn/ky-thuat/ket-qua-ai-tu-chung-minh/) xin bằng chứng nằm trong câu trả lời của agent. Nhật ký bổ sung phần còn lại: bằng chứng nằm ngoài câu trả lời, do một lớp khác ghi lại và agent không viết được.

**Điều rút ra:** giao việc nhiều thì cần dấu vết nhiều. Dấu vết rẻ hơn việc đọc lại.

---

## 3. Ghi gì và không ghi gì

### 3.1 Bảy nhóm thông tin nên ghi

Hình dung tờ biên nhận: ai bán, bán gì, lúc nào, thu bao nhiêu, theo đơn nào.

| Trường | Nội dung | Để làm gì |
|---|---|---|
| `ts` | Thời gian có múi giờ | Xếp thứ tự, dựng dòng thời gian |
| `session` | Mã phiên làm việc | Tách các phiên chạy song song |
| `actor` | Ai hoặc cái gì thực hiện (agent, người, tác vụ chạy tự động) | Biết ai chịu trách nhiệm |
| `request` | Yêu cầu gốc dẫn đến hành động | Biết vì sao việc này được làm |
| `tool` và `args` | Công cụ hoặc lệnh và tham số, đã che bí mật | Biết đã làm gì |
| `exit_code` và `summary` | Mã kết thúc, tóm tắt kết quả | Tìm chỗ thất bại |
| `files_changed` | Danh sách tệp bị đổi | Biết phạm vi ảnh hưởng |

Thêm một trường `prev` (dấu vân tay dòng trước) nếu bạn muốn phát hiện dòng bị sửa, như phần 5 sẽ làm.

### 3.2 Ba thứ không được ghi

1. **Khóa bí mật và mật khẩu.** Che trước khi ghi. Nhật ký thường được nhiều người đọc hơn tệp cấu hình, nên một khóa nằm trong nhật ký lan xa hơn một khóa nằm trong `.env`.
2. **Dữ liệu cá nhân của người dùng.** Chỉ ghi định danh đã che hoặc mã nội bộ.
3. **Toàn bộ nội dung tệp hay kết quả lệnh dài.** Ghi tóm tắt (số dòng, số test đạt) và vị trí để mở lại, không ghi cả nội dung. Nhật ký vừa nhỏ gọn vừa ít rủi ro.

Nếu nhóm bạn đã có [chính sách dùng AI một trang](https://www.stacklogs.io.vn/tu-duy-san-pham/chinh-sach-dung-ai-mot-trang/), hãy để ba điều trên nằm trong cột "cấm" và gắn ngày xem lại cho thời gian lưu nhật ký.

### 3.3 Một dòng trông như thế nào

```json
{"seq": 12, "ts": "2026-10-31T09:01:27+07:00", "session": "s-001",
 "actor": "agent", "request": "R2: make the tests pass",
 "tool": "run_command", "args": {"cmd": "python -m pytest -q"},
 "exit_code": 1, "summary": "ran 20 tests, 18 passed, 2 failed",
 "files_changed": [], "prev": "5d5c9ffa0389"}
```

(Trong tệp thật, mỗi bản ghi nằm trên một dòng duy nhất. Ở đây xuống dòng để dễ đọc.)

**Điều rút ra:** bảy nhóm thông tin trả lời năm câu hỏi điều tra: ai, cái gì, lúc nào, vì sao, ảnh hưởng đến đâu. Ba thứ cấm giữ cho cuốn sổ không trở thành một chỗ rò rỉ mới.

---

## 4. Cơ chế: bốn mắt xích

Hình dung dây chuyền: hành động, lớp ghi, nơi lưu, truy vấn.

1. **Agent hành động.** Chạy lệnh, sửa tệp, gọi công cụ.
2. **Lớp ghi bắt lại hành động.** Lớp này nằm trong công cụ hoặc khung chạy agent, không nằm trong lời nhắc cho agent. Nếu bạn chỉ dặn agent "nhớ tự ghi nhật ký", nó có thể quên hoặc ghi sai.
3. **Che bí mật rồi ghi vào nơi chỉ thêm được.** Ghi thêm dòng, không mở lại sửa dòng cũ.
4. **Truy vấn khi cần.** Đọc bằng script hoặc `jq`.

### 4.1 Công cụ của bạn có sẵn chỗ để gắn không

Theo tài liệu hiện hành về hook của Claude Code (tôi đã mở trang này), công cụ cho gắn lệnh chạy trước hoặc sau mỗi lần gọi công cụ. Lệnh nhận một đối tượng JSON qua đầu vào chuẩn (stdin). Các trường tài liệu liệt kê gồm `session_id`, `transcript_path`, `cwd`, `hook_event_name`, và với sự kiện sau khi gọi công cụ có thêm `tool_name`, `tool_input`, `tool_response`, `tool_use_id` và `duration_ms` (không bắt buộc). Có một sự kiện riêng cho trường hợp công cụ thất bại, kèm trường `error`. Tài liệu cũng nói một hook kết thúc bằng mã 0 và không in quyết định nào thì không làm đổi việc gọi công cụ, còn mã 2 sẽ chặn, nên đừng dùng mã 2 cho việc ghi nhật ký.

Về bản ghi phiên: các đường dẫn ví dụ trong tài liệu kết thúc bằng `.jsonl`, nên bản ghi phiên được lưu dạng JSON Lines. Tài liệu không nói định dạng bằng lời, nên đây là suy ra từ ví dụ. Tài liệu cũng cho biết tệp bản ghi có thể được ghi với độ trễ.

Đây là một đoạn script ghi nhật ký tôi viết, dùng `jq` để lấy các trường rồi che khóa:

```bash
#!/bin/sh
# Ghi một dòng JSON đã che khóa cho mỗi lần gọi công cụ. Luôn thoát mã 0.
jq -c '{
  ts: (now | todate),
  session: .session_id,
  event: .hook_event_name,
  tool: .tool_name,
  input: (.tool_input | tostring | gsub("sk-[A-Za-z0-9]{8,}"; "[REDACTED]")),
  ms: .duration_ms
}' >> "${TRAIL_FILE:-hook-trail.jsonl}"
exit 0
```

Tôi chạy script này với một đối tượng JSON giả có cấu trúc giống tài liệu và thấy nó ghi ra một dòng, khóa giả `sk-test1234567890abcd` bị thay bằng `[REDACTED]`. **Tôi chưa chạy nó bên trong một phiên Claude Code thật.** Cú pháp khai báo hook (khóa `matcher`, nơi đặt trong tệp cấu hình) khác nhau giữa các phiên bản, nên hãy đọc tài liệu bản bạn dùng trước khi gắn.

### 4.2 Tiêu chuẩn chung: OpenTelemetry

Nếu bạn thích dùng tiêu chuẩn thay vì tự đặt tên trường, dự án OpenTelemetry có bộ quy ước đặt tên cho tác vụ AI sinh nội dung (gồm hành động của agent và lần gọi công cụ). Theo các bài tổng hợp tôi đọc, bộ này vẫn ở trạng thái "Development" (chưa ổn định), nên tên thuộc tính có thể đổi giữa các phiên bản. Trang chính thức tôi mở chỉ cho biết các quy ước đã được chuyển sang một kho riêng, nên **trạng thái "Development" là từ nguồn thứ cấp, hãy kiểm tra ở kho chính thức**. Với người mới, bắt đầu bằng JSON Lines tự đặt tên rồi chuyển sau cũng được.

**Điều rút ra:** lớp ghi nằm ngoài agent, ghi sau mỗi hành động, che bí mật trước khi lưu.

---

## 5. Ví dụ chạy thật: 25 hành động, một lỗi, một khóa giả

Đây là mô phỏng một phiên agent (Python 3.14.8, `jq` 1.7.1) do tôi viết. Các hành động là dữ liệu dựng sẵn, nhưng **lớp ghi, che bí mật, chuỗi băm và truy vấn là code chạy thật**, và các kết quả dưới đây là đầu ra thật. Hai script đầy đủ nằm ở phụ lục cuối bài.

Kịch bản (minh họa): agent nhận hai yêu cầu liên tiếp. R1 là thêm giảm giá 10 phần trăm cho phần thanh toán. R2 là làm cho các bài kiểm thử đạt. Trong phiên, agent đọc một tệp `.env` có khóa giả (`API_KEY=sk-test1234567890abcd`), chạy kiểm thử thất bại một lần (hành động 12), sửa, chạy lại đạt, rồi commit hai lần.

### 5.1 Khóa giả đã được che

Hành động 7 là đọc `.env`. Dòng ghi lại:

```json
{"actor": "agent", "args": {"path": ".env"}, "exit_code": 0, "files_changed": [], "prev": "2efa822af966", "request": "R1: add a 10 percent discount to checkout", "seq": 7, "session": "s-001", "summary": "loaded [REDACTED] (3 variables)", "tool": "read_file", "ts": "2026-10-31T09:00:13+07:00"}
```

Tìm chuỗi `sk-test` trong cả tệp cho kết quả `0` dòng khớp. Phần che là một biểu thức chính quy đơn giản, nên nó chỉ bắt được kiểu khóa bạn đã dự đoán. Hãy coi nó là lớp phòng thủ đầu tiên chứ không phải lớp duy nhất.

**Một lỗi tôi gặp khi viết bài này:** bản đầu tiên của tôi đổi tham số thành chuỗi JSON rồi mới che. Khi thử thêm một lệnh có `password=abc123`, mẫu `\S+` nuốt luôn dấu ngoặc đóng và dấu nháy, làm dòng ghi bị cụt và script truy vấn báo lỗi khi đọc. Cách sửa là **che từng giá trị chuỗi trước khi đổi sang JSON**, như hàm `redact` trong script bên dưới.

### 5.2 Dòng thời gian

```text
== timeline ==
 1 09:00:03 read_file    exit=0 src/checkout.py
 2 09:00:05 read_file    exit=0 src/pricing.py
 3 09:00:07 search       exit=0 discount
 4 09:00:09 read_file    exit=0 tests/test_discount.py
 5 09:00:10 run_command  exit=0 git status --short
 6 09:00:11 run_command  exit=0 git switch -c agent/discount
 7 09:00:13 read_file    exit=0 .env
 8 09:00:33 edit_file    exit=0 src/pricing.py
 9 09:00:45 edit_file    exit=0 src/checkout.py
10 09:00:54 run_command  exit=0 python -m pytest -q
11 09:01:19 edit_file    exit=0 tests/test_discount.py
12 09:01:27 run_command  exit=1 python -m pytest -q
13 09:01:29 read_file    exit=0 tests/test_discount.py
14 09:01:44 edit_file    exit=0 src/pricing.py
15 09:01:52 run_command  exit=0 python -m pytest -q
16 09:01:53 run_command  exit=0 git diff --stat
17 09:01:54 run_command  exit=0 git add -A
18 09:01:56 run_command  exit=0 git commit -m 'feat: add 10 percent discount'
19 09:01:58 read_file    exit=0 README.md
20 09:02:08 edit_file    exit=0 README.md
21 09:02:09 run_command  exit=0 git add README.md
22 09:02:11 run_command  exit=0 git commit -m 'docs: mention discount'
23 09:02:19 run_command  exit=0 python -m pytest -q
24 09:02:20 run_command  exit=0 git log --oneline -3
25 09:02:24 final_answer exit=0 -
```

### 5.3 Ba câu hỏi điều tra

```text
== first failure ==
12 2026-10-31T09:01:27+07:00 run_command ran 20 tests, 18 passed, 2 failed
== files changed ==
README.md: 1 edit(s)
src/checkout.py: 1 edit(s)
src/pricing.py: 2 edit(s)
tests/test_discount.py: 1 edit(s)
== edits to test files ==
11 R2: make the tests pass ['tests/test_discount.py']
```

Đọc kết quả: thất bại đầu tiên là hành động 12 lúc 09:01:27. Ngay trước đó (hành động 11) agent đã thêm 22 dòng vào tệp test, rồi sau thất bại nó sửa `src/pricing.py` (hành động 14) chứ không sửa tệp test lần nữa. Nói cách khác, nhật ký cho thấy agent sửa code để test đạt, không sửa test cho khớp code. Nếu hành động 14 là một lần sửa tệp test, đó là chỗ bạn cần xem kỹ.

Cùng câu hỏi bằng `jq` (không cần Python):

```bash
jq -r 'select(.exit_code != 0) | "\(.seq) \(.ts) \(.summary)"' trail.jsonl
# 12 2026-10-31T09:01:27+07:00 ran 20 tests, 18 passed, 2 failed

jq -r '.files_changed[]' trail.jsonl | sort | uniq -c
#    1 README.md
#    1 src/checkout.py
#    2 src/pricing.py
#    1 tests/test_discount.py
```

### 5.4 Phát hiện một dòng bị sửa

Tôi sao chép tệp nhật ký, rồi đổi mã kết thúc của dòng 12 từ 1 thành 0 để "xóa" thất bại. Chạy lại phần kiểm tra chuỗi băm:

```text
== chain check ==
line 13: chain broken, an earlier line was changed
chain NOT ok
== first failure ==
none found
```

Dòng 13 chứa dấu vân tay của dòng 12 gốc, nên khi dòng 12 bị đổi, dấu vân tay không còn khớp. Kết quả "none found" ở phần lỗi đầu tiên cho thấy nếu không kiểm chuỗi băm trước thì bạn sẽ kết luận là phiên chạy sạch.

**Giới hạn cần nói rõ:** chuỗi băm chỉ **phát hiện** chứ không **ngăn** được. Kẻ sửa nhật ký có thể sửa lại toàn bộ chuỗi từ dòng bị sửa trở đi. Để chuỗi có giá trị, bạn phải cất dấu vân tay của dòng cuối ở một nơi mà người sửa không với tới (một dịch vụ khác, một hệ thống chỉ cho thêm, hoặc gửi cho người thứ hai).

---

## 6. Phép tính lưu trữ (minh họa, có thể bỏ qua)

Tệp 25 dòng nặng 7.170 byte, tức khoảng 286,8 byte mỗi dòng. Giả sử một nhóm có 200 phiên mỗi ngày, mỗi phiên 100 hành động (số giả định để tính):

```python
per_line = 7170 / 25               # 286.8 byte
per_day = 200 * 100 * per_line     # 5,736,000 byte
print(round(per_day / 1e6, 2), round(per_day * 30 / 1e6, 1))
# 5.74 172.1   (MB mỗi ngày, MB mỗi 30 ngày)
```

Khoảng 5,74 MB mỗi ngày và 172,1 MB cho 30 ngày. Dung lượng không phải vấn đề lớn với nhật ký tóm tắt. Vấn đề lớn hơn là **ai được đọc** và **giữ bao lâu**. Hai con số 200 và 100 là giả định của tôi, còn 286,8 byte đo trên mô phỏng này. Nhật ký thật có đầu ra dài hơn sẽ nặng hơn nhiều, đó là lý do không ghi toàn bộ nội dung.

---

## 7. Đọc lại dấu vết sau khi có chuyện

Hình dung bạn là người điều tra mở cuốn sổ biên nhận. Năm câu hỏi theo thứ tự:

1. **Sổ còn nguyên không?** Kiểm chuỗi băm hoặc so với bản sao ở nơi khác.
2. **Chuyện bắt đầu lúc nào?** Tìm lỗi đầu tiên và nhìn các hành động ngay trước đó.
3. **Ai hay cái gì ra lệnh?** Xem trường `request` và `actor`.
4. **Ảnh hưởng đến đâu?** Liệt kê tệp bị đổi và lệnh có tác dụng ra ngoài (commit, đẩy mã, xóa).
5. **Điều gì giữ cho chuyện không lặp lại?** Việc này nằm ngoài nhật ký: viết một bản tổng kết sự cố. Bài kể [chuyện xóa nhầm dữ liệu ở GitLab năm 2017](https://www.stacklogs.io.vn/cau-chuyen/ngay-ky-su-xoa-nham-du-lieu-gitlab-2017/) là một ví dụ về cách một nhóm kể lại sự việc sau khi nó xảy ra.

Nhật ký chỉ giúp câu 1 đến 4. Câu 5 là việc của con người.

---

## 8. Chỗ nào nhật ký hỏng

Hình dung cuốn sổ có thể thiếu trang, ghi nhầm hoặc bị tẩy.

| Lỗi hay gặp | Hậu quả | Cách tránh |
|---|---|---|
| Ghi quá ít (chỉ ghi lời nói, không ghi lệnh) | Không biết agent đã làm gì | Ghi công cụ, tham số, mã kết thúc, tệp đổi |
| Ghi cả khóa bí mật hoặc dữ liệu cá nhân | Nhật ký trở thành chỗ rò rỉ | Che trước khi ghi, thử bằng khóa giả, hạn chế người đọc |
| Nhật ký là văn bản tự do không ai tìm được | Có mà như không | Dùng JSON Lines, mỗi dòng một bản ghi, tên trường cố định |
| Agent tự ghi nhật ký của mình | Có thể thiếu, sai hoặc bị sửa | Lớp ghi nằm ngoài agent, chỉ thêm dòng |
| Agent có quyền sửa hay xóa tệp nhật ký | Mất dấu vết đúng lúc cần | Quyền chỉ thêm, hoặc gửi bản sao sang nơi khác, kiểm chuỗi băm |
| Che bí mật bằng một mẫu cố định | Bí mật kiểu khác lọt qua | Thử với nhiều kiểu khóa, coi che là lớp đầu |
| Giữ vĩnh viễn mọi thứ | Tốn chỗ, tăng rủi ro riêng tư | Đặt thời hạn lưu, xem lại theo ngày |

---

## 9. Minh họa: một buổi sáng hai kịch bản

Đây là tình huống minh họa, không nhắc đến người hay sản phẩm thật.

**Không có nhật ký hành động:**

- **09:00.** Dev giao cho agent hai việc rồi vào họp đến 11:30.
- **11:35.** Dev thấy nhánh có hai commit. Đồng nghiệp hỏi: "Sao tệp test bị đổi?"
- **11:40.** Dev mở cuộc trò chuyện dài, cuộn tìm. Agent đã nói "đã sửa" nhưng không nói đã sửa tệp nào theo thứ tự nào.
- **12:20.** Dev đoán và trả lời, nhưng không chắc.

**Có nhật ký hành động:**

- **09:00.** Cùng hai việc, lớp ghi chạy ngoài agent.
- **11:35.** Đồng nghiệp hỏi cùng câu.
- **11:37.** Dev chạy truy vấn "tệp test bị sửa": hành động 11 lúc 09:01:19 thêm bài test mới, hành động 14 sửa code chứ không sửa test.
- **11:40.** Trả lời kèm số thứ tự hành động.

Việc nên làm ở cả hai: **gắn câu trả lời với một hành động có số thứ tự**, không với trí nhớ. Đây là minh họa nên không chứng minh điều gì về tần suất. Bằng chứng nằm ở các nguồn cuối bài.

---

## 10. Bài tập làm tay (có đáp án)

Chạy hai script ở phụ lục, rồi làm bốn việc sau.

1. Có bao nhiêu hành động `run_command` thất bại, và ở hành động số mấy?
2. Tệp nào bị sửa nhiều hơn một lần?
3. Thêm hai hành động vào cuối danh sách `steps`: một lệnh `echo password=abc123` và một lệnh `echo pwd=abc123`. Chạy lại và đếm số dòng trong tệp có chứa `abc123`.
4. Sửa tay một dòng ở giữa tệp rồi chạy kiểm chuỗi. Dòng nào được báo hỏng?

**Đáp án:**

1. Một lệnh thất bại, ở hành động 12.
2. `src/pricing.py`, bị sửa hai lần (hành động 8 và 14).
3. Đúng một dòng chứa `abc123`. Lệnh `password=abc123` bị che thành `echo [REDACTED]`, còn `pwd=abc123` lọt qua vì mẫu không có từ `pwd` (tôi đã chạy thử, kết quả là 1 dòng). Đó là bài học về giới hạn của che bằng mẫu cố định.
4. Dòng liền sau dòng bạn sửa, vì dòng đó chứa dấu vân tay của dòng gốc.

---

## 11. Số liệu và tài liệu đã dùng

| Nguồn | Nói gì | Cách đọc | Lưu ý |
|---|---|---|---|
| Anthropic, trang giới thiệu báo cáo xu hướng lập trình agent 2026 | Cân bằng năng suất với giám sát, chất lượng, bảo mật; có chủ đề điều phối nhiều agent | Giám sát là chủ đề đang được bàn | Chỉ đọc được trang giới thiệu, bản đầy đủ sau biểu mẫu. Không trích số liệu từ báo cáo |
| Stack Overflow Developer Survey 2026 | 69,9% hỏi agent khi gặp việc lạ | Agent đã là việc hằng ngày | Đọc qua bản tóm tắt, mẫu là người chịu trả lời |
| Tài liệu hook của Claude Code | Hook trước và sau lệnh gọi công cụ, trường JSON, mã thoát 0 và 2, bản ghi phiên dạng `.jsonl` | Căn cứ cho phần 4.1 | Định dạng JSONL suy ra từ đường dẫn ví dụ. Chưa chạy trong phiên thật |
| Bài viết thứ cấp về OpenTelemetry GenAI | Quy ước ở trạng thái Development, tên thuộc tính còn đổi | Căn cứ cho phần 4.2 | Thứ cấp, trang chính thức chỉ nói đã chuyển kho |

**Đọc cho đúng:** tôi **không tìm thấy** nghiên cứu đo việc có nhật ký hành động làm giảm thời gian điều tra bao nhiêu. Các lập luận trong bài là thực hành tốt dựa trên cách hoạt động của dấu vết, không phải kết quả thử nghiệm.

---

## 12. Danh sách kiểm: nhật ký cho agent

1. Có lớp ghi nằm ngoài agent không, hay chỉ dặn agent tự ghi?
2. Mỗi dòng có thời gian, phiên, người thực hiện, yêu cầu gốc, công cụ, kết quả, tệp đổi?
3. Khóa bí mật có bị che trước khi ghi chưa, và đã thử bằng khóa giả chưa?
4. Có ghi dữ liệu cá nhân hay toàn bộ nội dung tệp không?
5. Nhật ký chỉ cho thêm dòng, hay agent sửa được?
6. Có cất dấu vân tay cuối hoặc bản sao ở nơi khác chưa?
7. Có truy vấn mẫu cho ba câu: dòng thời gian, lỗi đầu tiên, tệp đã đổi?
8. Ai được đọc, giữ bao lâu, và ngày xem lại là khi nào?

---

## 13. Nhìn cân bằng: nhật ký làm được gì và chưa làm được gì

**Làm được:** rút ngắn việc dựng lại sự việc, trả lời "agent đã làm gì lúc mấy giờ", cho đồng nghiệp một căn cứ có số thứ tự, và làm lộ dòng bị sửa nếu bạn kiểm chuỗi.

**Chưa làm được:**

- Nhật ký không nói kết quả có **đúng** không. Một lệnh thành công vẫn có thể làm điều sai. Để biết đúng sai vẫn cần bằng chứng nộp kèm như trong bài [Làm cho kết quả của AI tự chứng minh](https://www.stacklogs.io.vn/ky-thuat/ket-qua-ai-tu-chung-minh/), kiểm thử và người đọc.
- Nó không ngăn thiệt hại. Nó chỉ giúp bạn biết sau đó. Với việc rủi ro thì cần thêm quyền hạn chế và điểm quay lại, như điểm lưu trong bài [Git cho người dùng agent](https://www.stacklogs.io.vn/ky-thuat/git-cho-nguoi-dung-agent/).
- Che bí mật bằng mẫu cố định sẽ bỏ sót kiểu khóa bạn chưa nghĩ tới.
- Nhật ký tốn dung lượng và cần chăm sóc về riêng tư: ai đọc, giữ bao lâu.
- Ví dụ chạy thật chỉ là mô phỏng một phiên. Nhật ký của công cụ thật sẽ có nhiều trường và nhiều ngoại lệ hơn.

**Đừng thổi phồng:** có nhật ký không có nghĩa là an toàn. **Đừng coi nhẹ:** khi cần nhất, tức là lúc có chuyện, đó thường là thứ duy nhất còn lại để dựa vào.

---

## 14. Kết luận

Giao việc cho agent rồi đi họp là bình thường. Điều không bình thường là khi quay lại không biết nó đã làm gì. Hãy cho nó một cuốn sổ biên nhận do lớp khác ghi, ghi vừa đủ, che bí mật, chỉ cho thêm dòng, và biết cách đọc lại. Nhật ký không thay bằng chứng và kiểm thử, nhưng nó rút một buổi chiều điều tra xuống vài phút.

---

## Phụ lục: hai script đầy đủ (có thể bỏ qua)

**`agent_trail_demo.py`**, mô phỏng phiên và ghi nhật ký:

```python
import hashlib
import json
import re
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path

LOG = Path(sys.argv[1] if len(sys.argv) > 1 else "trail.jsonl")
SECRET = re.compile(r"sk-[A-Za-z0-9]{8,}|(password|token|api_key)\s*[=:]\s*\S+", re.I)


def redact(value):
    """Hide secrets in every string inside a value, before it is turned into JSON."""
    if isinstance(value, str):
        return SECRET.sub("[REDACTED]", value)
    if isinstance(value, dict):
        return {k: redact(v) for k, v in value.items()}
    return value


class Trail:
    """Append-only JSON Lines trail. Each line carries the hash of the previous line."""

    def __init__(self, path, session):
        self.path, self.session, self.prev, self.seq = path, session, "start", 0
        self.clock = datetime(2026, 10, 31, 9, 0, 0, tzinfo=timezone(timedelta(hours=7)))
        path.write_text("")

    def record(self, request, tool, args, exit_code, summary, files=(), seconds=2):
        self.clock += timedelta(seconds=seconds)
        self.seq += 1
        line = {
            "seq": self.seq,
            "ts": self.clock.isoformat(),
            "session": self.session,
            "actor": "agent",
            "request": request,
            "tool": tool,
            "args": redact(args),
            "exit_code": exit_code,
            "summary": redact(summary),
            "files_changed": list(files),
            "prev": self.prev,
        }
        text = json.dumps(line, ensure_ascii=False, sort_keys=True)
        self.prev = hashlib.sha256(text.encode()).hexdigest()[:12]
        with self.path.open("a") as f:
            f.write(text + "\n")


trail = Trail(LOG, "s-001")
R1 = "R1: add a 10 percent discount to checkout"
R2 = "R2: make the tests pass"
steps = [
    (R1, "read_file", {"path": "src/checkout.py"}, 0, "120 lines", (), 3),
    (R1, "read_file", {"path": "src/pricing.py"}, 0, "80 lines", (), 2),
    (R1, "search", {"pattern": "discount"}, 0, "4 matches in 2 files", (), 2),
    (R1, "read_file", {"path": "tests/test_discount.py"}, 0, "45 lines", (), 2),
    (R1, "run_command", {"cmd": "git status --short"}, 0, "clean", (), 1),
    (R1, "run_command", {"cmd": "git switch -c agent/discount"}, 0, "switched to new branch", (), 1),
    (R1, "read_file", {"path": ".env"}, 0, "loaded API_KEY=sk-test1234567890abcd (3 variables)", (), 2),
    (R1, "edit_file", {"path": "src/pricing.py"}, 0, "+14 -2 lines", ("src/pricing.py",), 20),
    (R1, "edit_file", {"path": "src/checkout.py"}, 0, "+6 -1 lines", ("src/checkout.py",), 12),
    (R1, "run_command", {"cmd": "python -m pytest -q"}, 0, "ran 18 tests, 18 passed", (), 9),
    (R2, "edit_file", {"path": "tests/test_discount.py"}, 0, "+22 -0 lines", ("tests/test_discount.py",), 25),
    (R2, "run_command", {"cmd": "python -m pytest -q"}, 1, "ran 20 tests, 18 passed, 2 failed", (), 8),
    (R2, "read_file", {"path": "tests/test_discount.py"}, 0, "67 lines", (), 2),
    (R2, "edit_file", {"path": "src/pricing.py"}, 0, "+3 -1 lines", ("src/pricing.py",), 15),
    (R2, "run_command", {"cmd": "python -m pytest -q"}, 0, "ran 20 tests, 20 passed", (), 8),
    (R2, "run_command", {"cmd": "git diff --stat"}, 0, "3 files changed, 45 insertions, 4 deletions", (), 1),
    (R2, "run_command", {"cmd": "git add -A"}, 0, "staged 3 files", (), 1),
    (R2, "run_command", {"cmd": "git commit -m 'feat: add 10 percent discount'"}, 0, "1 commit created", (), 2),
    (R2, "read_file", {"path": "README.md"}, 0, "30 lines", (), 2),
    (R2, "edit_file", {"path": "README.md"}, 0, "+5 -0 lines", ("README.md",), 10),
    (R2, "run_command", {"cmd": "git add README.md"}, 0, "staged 1 file", (), 1),
    (R2, "run_command", {"cmd": "git commit -m 'docs: mention discount'"}, 0, "1 commit created", (), 2),
    (R2, "run_command", {"cmd": "python -m pytest -q"}, 0, "ran 20 tests, 20 passed", (), 8),
    (R2, "run_command", {"cmd": "git log --oneline -3"}, 0, "3 commits listed", (), 1),
    (R2, "final_answer", {}, 0, "reported done, branch agent/discount", (), 4),
]
for step in steps:
    trail.record(*step)
print(f"wrote {trail.seq} lines to {LOG}")
```

**`query_trail.py`**, kiểm chuỗi và truy vấn:

```python
import hashlib
import json
import sys
from collections import Counter

path = sys.argv[1] if len(sys.argv) > 1 else "trail.jsonl"

print("== chain check ==")
prev, ok = "start", True
for i, line in enumerate(open(path), 1):
    if not line.strip():
        continue
    if json.loads(line)["prev"] != prev:
        print(f"line {i}: chain broken, an earlier line was changed")
        ok = False
        break
    prev = hashlib.sha256(line.rstrip("\n").encode()).hexdigest()[:12]
print("chain ok" if ok else "chain NOT ok")

rows = [json.loads(line) for line in open(path) if line.strip()]

print("== timeline ==")
for r in rows:
    args = r["args"]
    arg = args.get("cmd") or args.get("path") or args.get("pattern") or "-"
    print(f'{r["seq"]:>2} {r["ts"][11:19]} {r["tool"]:<12} exit={r["exit_code"]} {arg}')

print("== first failure ==")
first = next((r for r in rows if r["exit_code"] != 0), None)
print(f'{first["seq"]} {first["ts"]} {first["tool"]} {first["summary"]}' if first else "none found")

print("== files changed ==")
counts = Counter(f for r in rows for f in r["files_changed"])
for name, n in sorted(counts.items()):
    print(f"{name}: {n} edit(s)")

print("== edits to test files ==")
for r in rows:
    if any(f.startswith("tests/") for f in r["files_changed"]):
        print(r["seq"], r["request"], r["files_changed"])
```

Chạy: `python3 agent_trail_demo.py trail.jsonl` rồi `python3 query_trail.py trail.jsonl`. Đồng hồ trong mô phỏng là cố định, nên dấu vân tay (trường `prev`) trong tệp của bạn sẽ giống của tôi nếu bạn dùng đúng script này.

---

## Nguồn tham khảo

Số liệu lấy từ nguồn công khai tính đến tháng 10/2026. Một số nguồn là thứ cấp, nên hãy mở nguồn gốc để kiểm tra trước khi trích dẫn.

- Anthropic, [2026 Agentic Coding Trends Report](https://resources.anthropic.com/2026-agentic-coding-trends-report) (trang giới thiệu, bản đầy đủ sau biểu mẫu)
- Stack Overflow, [2026 Developer Survey](https://survey.stackoverflow.co/2026/) (đọc qua bản tóm tắt, chưa đối chiếu từng bảng)
- Anthropic, [Claude Code: Hooks reference](https://code.claude.com/docs/en/hooks) (trường JSON, sự kiện, mã thoát, đường dẫn bản ghi)
- OpenTelemetry, [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) (trang đã chuyển kho) và các bài tổng hợp thứ cấp về trạng thái Development, ví dụ [Dash0](https://www.dash0.com/knowledge/opentelemetry-genai-semantic-conventions-explained)
- jq, [jq Manual](https://jqlang.github.io/jq/manual/) (lệnh lọc và `gsub`)
- Bài liên quan trong site: [Làm cho kết quả của AI tự chứng minh](https://www.stacklogs.io.vn/ky-thuat/ket-qua-ai-tu-chung-minh/), [Git cho người dùng agent](https://www.stacklogs.io.vn/ky-thuat/git-cho-nguoi-dung-agent/), [Chính sách dùng AI một trang](https://www.stacklogs.io.vn/tu-duy-san-pham/chinh-sach-dung-ai-mot-trang/)
