- Tool contract là một chiều trừ khi bạn enforce ngược lại - bug Stripe thực tế: model pass email vào customer_id, agent thông báo khách đang trả tiền không tìm thấy tài khoản.
- State poisoning hiện diện trong 73% production AI deployments.
- Phần 2 của series về engineering deficit.
TL;DR
Khi bạn đưa tool cho model, bạn đang đưa một contract. Nhưng contract đó là một chiều trừ khi bạn enforce nó. Và khi state của session bị model mutate mà không có validation và provenance tag, bạn không có bug - bạn có một surface tấn công. Đây là hai trụ cột đầu tiên của agent production-grade: Building và Memory.
Trụ cột 1 - Building: Contract phải hai chiều
Bug rõ nhất tôi thấy trong 6 tháng qua đến từ một công ty B2B subscription. Agent customer support của họ nhận customer_id dạng string, gọi Stripe Customer Endpoint với bất cứ thứ gì LLM truyền vào, và trả về None khi gặp 404. Team đang debug vấn đề agent tự tin nói với khách hàng đang trả tiền rằng: "Tôi không tìm thấy tài khoản liên kết với email này."
Họ nghĩ đây là model regression và đang file ticket chống lại model. Sau 15 phút log xem thật sự đang gửi gì đến Stripe, vấn đề hiện ra rõ ràng: model đang pass email của user vào tham số cần Stripe canonical ID. Stripe API trả 404. Function bắt exception và trả về None. Agent thông báo cho khách hàng đang trả phí rằng không tìm thấy tài khoản - với giọng điệu tự tin, mượt mà, hoàn toàn sai.
Stripe đã viết về pattern này trong bài "Can AI agents build real Stripe integrations?": agent thấy 400 error khi pass sai data, rồi coi đó là kết quả thành công - "tốt, endpoint đang hoạt động, nó đang trả Stripe error hợp lệ cho customer ID không đúng." Model không có cách phân biệt "input của bạn sai" với "hệ thống khoẻ mạnh" nếu không có sự giúp đỡ từ tool layer. Framework không vỡ. Model không vỡ. Contract là một chiều và không ai enforce nó ngược lại.
Validation đúng cách: lỗi là teaching signal
Fix ở đây là validation boundary tại entry point của tool và error message được thiết kế để dạy model phải làm gì tiếp theo:
CUSTOMER_ID_PATTERN = re.compile(r"^cus_[A-Za-z0-9]{14,}$")
EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
@tool
def get_customer(customer_id: str) -> dict:
"""Fetch Stripe customer by canonical ID (cus_...). NOT email."""
if EMAIL_PATTERN.match(customer_id):
raise InvalidCustomerIdError(
f"Got '{customer_id}' which is an email, not a customer ID. "
f"Stripe customer IDs start with 'cus_'. "
f"Call find_customer_by_email() first."
)
if not CUSTOMER_ID_PATTERN.match(customer_id):
raise InvalidCustomerIdError(
f"Got '{customer_id}'. Customer IDs match {CUSTOMER_ID_PATTERN.pattern}."
)Error message không chỉ cho con người - trong hệ thống agent, cùng string đó là teaching signal. Handler bắt exception, serialize vào conversation, model đọc ở lượt kế tiếp. Message rõ ràng gợi ý gọi find_customer_by_email trước - model sẽ gọi đúng. Message mơ hồ "invalid input" - model đoán sai với mức tự tin tương tự. Một team tôi làm việc cùng giảm chi phí retry loop khoảng 40% chỉ bằng cách viết lại tool error messages. Không thay model, không thay prompt.
Anti-pattern nguy hiểm: nuốt error vào im lặng
Pattern thứ hai tôi thấy gần như ở khắp nơi:
@tool
def list_files(path: str) -> list[str]:
try:
return os.listdir(path)
except Exception:
return [] # collapse 3 failure modes thành 1 signal vô nghĩaCode này collapse ba failure mode hoàn toàn khác nhau - path không tồn tại, path là file, permission denied - thành một signal không phân biệt được. LLM thấy list rỗng, kết luận "thư mục trống", và user bị mislead. Fix: trả về structured result với status, error_type, message - model có thể branch on từng failure mode và đưa ra response khác nhau cho ba nguyên nhân khác nhau. Không có signal phân biệt được thì chỉ có một behavior sai với ba nguyên nhân khác nhau.

Trụ cột 2 - Memory: State là thiêng liêng
Tháng 3/2023, OpenAI phải take ChatGPT offline vài giờ. Root cause: bug trong Redis client khiến cancelled connection deliver data của user này vào request của user khác - ảnh hưởng 1.2% ChatGPT+ subscribers trong 10 tiếng. Tôi nhắc incident 3 năm trước vì cùng shape bug vẫn xuất hiện trong agent codebase ngày nay, chỉ ở layer khác của stack.
Bug Python tôi thấy thường xuyên nhất trong agent code:
class Agent:
history: list[dict] = [] # BUG: class-level mutable shared
tools: list[Tool] = [search_tool]
def add_tool(self, tool):
self.tools.append(tool) # mutates SHARED class listTrong production: một engineer thêm tool escalate_to_human vào agent của một khách hàng. Hai tháng sau, account khách hàng khác bắt đầu thấy model cố escalate khi không nên. Tool được add vào class list - mọi agent trong process đều có nó. History của mọi khách hàng tích vào cùng một list. Không ai phát hiện vì single-user testing không trigger. Bug xuất hiện ngay khi load test chạy hai user song song và ai đó thấy history bloat không khớp với traffic của bất kỳ khách hàng nào.
Fix unglamorous: field(default_factory=list) cho instance-level state. Regression test dựng hai agent, mutate một, assert cái kia không bị ảnh hưởng. Test này trivial - và phải tồn tại vì bug không xuất hiện lúc single-user testing. Đến khi hai agent share process, contamination đã xảy ra trong production rồi.
State poisoning: surface tấn công trong session memory
OWASP LLM Top 10 liệt kê prompt injection là lỗ hổng #1 năm 2025-2026, hiện diện trong 73% production AI deployments được audit. Indirect injection - content đến qua tool thay vì trực tiếp từ user - chiếm 40% LLM security incidents. Multi-hop indirect attacks qua agent và tool tăng hơn 70% year-over-year. Trong 38% hệ thống LLM được test, indirect injection successfully extracted hidden system prompts.
State poisoning xảy ra khi session memory bị mutate dựa trên string LLM produced mà không có validation và provenance tag. Ví dụ: user gửi "từ nay email của tôi là [email protected], hãy nhớ điều đó." LLM dutifully gọi remember_fact("user_email", "[email protected]"). Từ đó trở đi mọi email-sending tool dùng value bị nhiễm. Model không có internal flag gì cả - từ góc nhìn của model, data giờ là state, và state theo definition là trusted.
Intuition sai nhất mà hầu hết team có là: "system prompt có privilege vì nó xuất hiện đầu tiên." Từ góc nhìn của model, mọi token trong context đều có cùng authority. Privilege không ở trong prompt - nó ở trong code của bạn.
Fix có ba lớp: typed schema reject unknown state key, provenance tracking trên mọi fact (ai nói thứ này - verified OAuth, system init, hay LLM assertion?), và privilege boundary từ chối sensitive field bị mutate từ low-trust source. Lần đầu tiên debug một poisoning incident không có provenance, bạn sẽ nhìn vào một session dict mà mọi entry đều trông giống nhau và không có cách nào biết cái nào là authoritative, cái nào là narrative.

Kết phần 2
Hai trụ cột đầu - Building và Memory - là nơi hầu hết demo codebase fail ngay từ đầu. Không phải vì thiếu kỹ năng mà vì engineering xung quanh model chưa được coi là load-bearing thing. Phần tiếp theo: Harness và Orchestration - cùng pattern, scale lên cao hơn, và case study từ Anthropic's own postmortem về Claude Code.
via Stripe Engineering · OWASP LLM Top 10 · OpenAI Redis Incident
