Khi một hệ thống multi-agent (đa agent) phát triển vượt quá vài task đơn giản, một vấn đề âm thầm nhưng rất tốn kém sẽ xuất hiện: mỗi sub-agent (agent con) được orchestrator (bộ điều phối) gọi tới đều nhận toàn bộ lịch sử hội thoại, toàn bộ tool output, toàn bộ tài liệu liên quan — dù nó chỉ cần giải quyết một mảnh việc rất nhỏ. Kết quả là context window (cửa sổ ngữ cảnh) bị nhồi đầy thông tin không liên quan, token bị đốt một cách vô ích, và chất lượng suy luận của agent giảm sút vì phải "lọc" quá nhiều nhiễu trước khi chạm tới phần việc thật sự cần làm.
Bài học này đi sâu vào kỹ thuật scoped agent session (phiên làm việc có phạm vi giới hạn) — cách thiết kế để mỗi sub-agent chỉ nhận đúng, đủ lượng context cần thiết để hoàn thành nhiệm vụ của nó, không hơn không kém. Đây là một trong những kỹ thuật tối ưu token có tác động lớn nhất trong agentic loop (vòng lặp agentic), đặc biệt khi hệ thống có nhiều tầng agent gọi lẫn nhau.
The Principle of Least Context
Principle of Least Context (nguyên tắc tối thiểu hóa ngữ cảnh) là một biến thể áp dụng cho AI agent của một nguyên lý đã tồn tại rất lâu trong bảo mật hệ thống: principle of least privilege (nguyên tắc đặc quyền tối thiểu). Trong bảo mật, nguyên tắc này nói rằng mỗi tiến trình, mỗi người dùng chỉ nên có đúng quyền truy cập cần thiết để thực hiện công việc của mình — không hơn. Áp dụng vào agentic system, nguyên tắc tương đương là: mỗi agent hoặc sub-agent chỉ nên nhận đúng phần context cần thiết để hoàn thành task được giao, không hơn.
Tại sao nguyên tắc này quan trọng đến vậy trong thời đại LLM (large language model)? Có ba lý do cốt lõi:
Thứ nhất, chi phí token tăng tuyến tính (hoặc tệ hơn) theo lượng context. Nếu orchestrator gọi 5 sub-agent, và mỗi sub-agent đều được "bơm" toàn bộ 15.000 token lịch sử hội thoại trước đó, bạn đang trả tiền cho 75.000 token context chỉ để 5 agent đó thực hiện 5 việc nhỏ — trong khi có thể mỗi agent chỉ cần 800-1.500 token liên quan trực tiếp đến task của nó.
Thứ hai, context dư thừa làm giảm chất lượng suy luận. Đây là điều nhiều kỹ sư đánh giá thấp. Nghiên cứu về "lost in the middle" (thông tin bị lạc mất ở giữa văn bản dài) cho thấy LLM có xu hướng chú ý tốt hơn vào phần đầu và phần cuối của context, và độ chính xác truy xuất thông tin giảm khi context giữa quá dài và chứa nhiều thông tin không liên quan. Một sub-agent sửa lỗi database query mà phải "lội" qua log CI/CD, thông tin deployment, và ghi chú họp team để tìm ra dòng SQL bị lỗi — rất dễ bị phân tán, trả lời sai hướng, hoặc "ảo giác" (hallucinate) ra giải pháp không liên quan.
Thứ ba, context dư thừa làm tăng rủi ro rò rỉ thông tin nhạy cảm. Nếu sub-agent xử lý một tác vụ công khai nhưng lại vô tình nhận được đoạn context có chứa API key, thông tin khách hàng, hoặc dữ liệu nội bộ nhạy cảm, agent có thể trích dẫn lại thông tin đó trong output — một rủi ro bảo mật thực sự, không chỉ là vấn đề hiệu năng.
Điểm quan trọng cần phân biệt: least context không đồng nghĩa với việc cắt bỏ thông tin một cách thô bạo. Mục tiêu không phải là gửi ít context nhất có thể, mà là gửi đúng context — đủ để agent suy luận chính xác, nhưng không có gì dư thừa gây nhiễu. Một sub-agent thiếu context quan trọng sẽ đưa ra quyết định sai; một sub-agent ngập trong context dư thừa cũng sẽ đưa ra quyết định sai, chỉ vì lý do khác. Đây là bài toán tối ưu hai chiều: tối thiểu hóa lượng token, tối đa hóa mật độ thông tin liên quan (relevance density).
Mẹo
- Trước khi thiết kế bất kỳ sub-agent nào, hãy viết ra một "context contract" (hợp đồng ngữ cảnh) — một danh sách rõ ràng: agent này cần biết gì để hoàn thành task, và không cần biết gì. Việc viết ra bằng văn bản giúp lộ diện ngay những giả định ngầm về context mà bạn từng nhồi vào "cho chắc".
- Đặt câu hỏi kiểm tra: "Nếu tôi xóa dòng context này, agent còn hoàn thành được task không?" Nếu câu trả lời là "vẫn được", đó là ứng viên để loại bỏ.
- Đo token đầu vào (input token) theo từng loại sub-agent trong hệ thống của bạn — nhiều team phát hiện ra rằng 60-80% context gửi cho sub-agent chuyên biệt hoàn toàn không liên quan đến task cụ thể của nó.
Architecture of a Scoped Session
Về mặt kiến trúc, một scoped session không phải là một cơ chế kỹ thuật đơn lẻ, mà là một lớp trung gian (mediation layer) đứng giữa orchestrator và sub-agent, có nhiệm vụ quyết định: trong toàn bộ state (trạng thái) và lịch sử hiện có, phần nào sẽ được đưa vào context của sub-agent sắp được gọi.
Có thể hình dung kiến trúc này gồm bốn thành phần chính:
1. Global state / shared memory (trạng thái toàn cục / bộ nhớ chia sẻ). Đây là "nguồn sự thật" (source of truth) chứa toàn bộ thông tin mà hệ thống multi-agent đã tích lũy được: lịch sử hội thoại với người dùng, kết quả từ các agent trước, dữ liệu lấy từ tool call, v.v. Global state thường rất lớn và không nên được truyền trực tiếp cho bất kỳ sub-agent nào.
2. Scope resolver (bộ giải quyết phạm vi). Đây là thành phần trung tâm của kiến trúc scoped session. Trước khi gọi một sub-agent, scope resolver nhận vào: (a) định nghĩa task mà sub-agent cần thực hiện, (b) toàn bộ global state hiện có, và trả về một context slice (lát cắt ngữ cảnh) — tập con dữ liệu tối thiểu cần thiết. Scope resolver có thể được lập trình cứng (rule-based) cho các trường hợp đã biết trước, hoặc dùng chính một LLM nhỏ, rẻ để thực hiện việc lọc/tóm tắt (sẽ nói ở phần Context Filtering).
3. Session boundary (ranh giới phiên). Mỗi sub-agent chạy trong một session riêng, có system prompt, lịch sử hội thoại, và bộ nhớ tạm (working memory) độc lập với session của orchestrator hoặc các sub-agent khác. Session boundary đảm bảo rằng khi sub-agent A xử lý xong, các token nó đã "tiêu thụ" trong quá trình suy luận (kể cả các bước trung gian, các lần gọi tool thất bại, chain-of-thought dài dòng) không bị mang sang session của sub-agent B hoặc trả về orchestrator một cách nguyên vẹn — chỉ có kết quả cuối cùng (kèm theo phần context tối thiểu để orchestrator hiểu kết quả đó) được đưa ngược lại.
4. Result projection (chiếu kết quả về). Sau khi sub-agent hoàn thành, kết quả trả về orchestrator cũng cần được lọc — không phải toàn bộ transcript suy luận của sub-agent, mà là một bản tóm tắt có cấu trúc: task đã hoàn thành hay chưa, output chính là gì, có lỗi/cảnh báo nào cần orchestrator biết không.
┌─────────────────────────────────────────────────────────┐
│ Global State │
│ (full conversation history, all tool outputs, │
│ all previous agent results, user context, etc.) │
└───────────────────────┬───────────────────────────────────┘
│
▼
┌───────────────┐
│ Scope Resolver │ <- decides what to keep
└───────┬───────┘
│ context slice (minimal, task-specific)
▼
┌─────────────────────┐
│ Sub-Agent Session │ <- isolated context window
│ (own system prompt, │
│ own working memory)│
└──────────┬──────────┘
│ result projection (compact summary)
▼
┌───────────────┐
│ Orchestrator │
└───────────────┘
Một điểm thiết kế quan trọng: scope resolver nên được xây dựng như một thành phần có thể tái sử dụng và kiểm thử độc lập, không phải logic viết tay, rời rạc trong mỗi lần gọi sub-agent. Khi hệ thống có 10-20 loại sub-agent khác nhau, việc có một scope resolver tập trung, có thể unit test, giúp bạn kiểm soát được ngân sách token (token budget) một cách nhất quán trên toàn hệ thống, thay vì mỗi nơi gọi sub-agent lại tự quyết định "gửi gì" theo cảm tính.
Mẹo
- Thiết kế session boundary sao cho việc "reset" một sub-agent (xóa toàn bộ lịch sử của nó và bắt đầu lại) là một phép toán rẻ và an toàn — điều này cực kỳ hữu ích khi sub-agent bị lạc hướng do context bị nhiễm (poisoned context) từ các bước suy luận sai trước đó.
- Log lại kích thước (số token) của context slice mà scope resolver tạo ra cho từng loại task. Đây là chỉ số bạn sẽ dùng để phát hiện "context creep" (context slice dần phình to theo thời gian) — một hiện tượng rất phổ biến khi team thêm dần "cho chắc" từng trường dữ liệu nhỏ vào mà không xem lại tổng thể.
- Đừng để result projection trở thành "cửa hậu" để leak toàn bộ transcript sub-agent về orchestrator — nếu bạn thấy result projection có kích thước gần bằng context slice đầu vào, khả năng cao là bước tóm tắt chưa được thực thi đúng.
Context Filtering: Delivering Only What the Sub-Agent Needs
Phần khó nhất của scoped session không phải là dựng kiến trúc — mà là quyết định: trong hàng chục nghìn token global state, cái gì thực sự cần được đưa vào context slice cho một sub-agent cụ thể? Có ba kỹ thuật lọc context được dùng phổ biến trong thực tế, từ đơn giản đến phức tạp, và chúng thường được kết hợp với nhau trong cùng một hệ thống.
Technique 1: Semantic Relevance Filtering
Semantic relevance filtering (lọc theo độ liên quan ngữ nghĩa) sử dụng embedding (vector biểu diễn ngữ nghĩa) hoặc một LLM nhỏ để đánh giá: đoạn thông tin nào trong global state có liên quan ngữ nghĩa đến task hiện tại, và chỉ giữ lại những đoạn vượt qua ngưỡng liên quan.
Cách vận hành phổ biến nhất: mỗi đơn vị thông tin trong lịch sử (một message, một tool result, một đoạn code) được lưu kèm embedding vector. Khi cần tạo context slice cho một sub-agent, ta tạo embedding cho mô tả task, rồi tính độ tương đồng cosine (cosine similarity) giữa task embedding và embedding của từng đơn vị thông tin, chỉ giữ lại top-k đơn vị có độ tương đồng cao nhất.
Ví dụ cụ thể: giả sử orchestrator cần gọi một sub-agent để sửa một database query bị lỗi. Global state chứa 40 message bao gồm: yêu cầu ban đầu của người dùng, log lỗi CI/CD, thảo luận về UI, một đoạn trao đổi về đặt tên biến, và đoạn log chứa câu SQL bị lỗi cùng traceback. Semantic filtering sẽ tự động ưu tiên các đoạn liên quan tới "database query", "SQL error", "traceback" và loại bỏ phần thảo luận UI, đặt tên biến — những thứ về mặt từ vựng có vẻ khác biệt hoàn toàn với domain của task.
from dataclasses import dataclass
from typing import List
import numpy as np
@dataclass
class ContextUnit:
id: str
text: str
embedding: np.ndarray
source: str # e.g. "ci_log", "user_message", "code_diff"
def semantic_filter(
task_description: str,
embed_fn,
context_units: List[ContextUnit],
top_k: int = 5,
min_similarity: float = 0.55,
) -> List[ContextUnit]:
"""Return only the context units semantically relevant to the task."""
task_vec = embed_fn(task_description)
scored = []
for unit in context_units:
sim = cosine_similarity(task_vec, unit.embedding)
if sim >= min_similarity:
scored.append((sim, unit))
scored.sort(key=lambda pair: pair[0], reverse=True)
return [unit for _, unit in scored[:top_k]]
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
task = "Fix the failing database query that returns duplicate rows on the orders table."
relevant_units = semantic_filter(
task_description=task,
embed_fn=get_embedding, # your embedding provider
context_units=global_state_units,
top_k=5,
min_similarity=0.55,
)
Kỹ thuật này đặc biệt hiệu quả khi global state lớn và không có cấu trúc rõ ràng (ví dụ: lịch sử chat tự do). Nhược điểm là cần duy trì một embedding index, và ngưỡng min_similarity cần được tinh chỉnh (tune) theo dữ liệu thực tế — ngưỡng quá cao sẽ bỏ lọt thông tin quan trọng, ngưỡng quá thấp sẽ không lọc được gì đáng kể.
Technique 2: Schema-Based Projection
Trong khi semantic filtering phù hợp với dữ liệu phi cấu trúc, phần lớn dữ liệu trong hệ thống thực tế (incident record, ticket, deployment log, database record) lại có cấu trúc rõ ràng — và với dữ liệu có cấu trúc, cách lọc hiệu quả và đáng tin cậy hơn nhiều là schema-based projection (chiếu theo schema): định nghĩa trước một schema đầu ra cho từng loại sub-agent, chỉ trích xuất đúng những field cần thiết, bỏ qua toàn bộ phần còn lại.
Ví dụ điển hình: một hệ thống DevOps có một "incident record" đầy đủ nặng khoảng 4.000 token, bao gồm: nội dung lỗi, stack trace, toàn bộ cấu hình CI/CD pipeline, lịch sử deploy 30 ngày gần nhất, metadata của từng thành viên team đã từng touch vào service đó, liên kết Slack thread, v.v. Một bug-fix agent (agent sửa lỗi) chỉ cần nội dung file code liên quan và thông tin lỗi — nó hoàn toàn không cần cấu hình CI/CD, lịch sử deploy, hay metadata team.
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class FullIncidentRecord:
"""~4,000 tokens when serialized: everything the incident
management system tracks for a single incident."""
incident_id: str
error_message: str
stack_trace: str
affected_file_path: str
affected_file_content: str
cicd_pipeline_config: dict # ~1,200 tokens — irrelevant to a code fix
deployment_history_30d: list # ~900 tokens — irrelevant to a code fix
team_metadata: dict # ~600 tokens — irrelevant to a code fix
slack_thread_links: list # ~300 tokens — irrelevant to a code fix
on_call_schedule: dict # ~400 tokens — irrelevant to a code fix
related_tickets: list # ~600 tokens — irrelevant to a code fix
@dataclass
class BugFixAgentContext:
"""~200 tokens when serialized: the schema a bug-fix agent
actually needs. Everything else is projected away."""
error_message: str
stack_trace: str
affected_file_path: str
affected_file_content: str
def project_for_bug_fix(record: FullIncidentRecord) -> BugFixAgentContext:
"""Schema-based projection: extract only the fields the
bug-fix agent's schema declares, discard the rest."""
return BugFixAgentContext(
error_message=record.error_message,
stack_trace=record.stack_trace,
affected_file_path=record.affected_file_path,
affected_file_content=record.affected_file_content,
)
scoped_context = project_for_bug_fix(full_incident_record)
Điểm mạnh của schema-based projection là tính dự đoán được (predictability): bạn biết chính xác context slice sẽ chứa gì, kích thước tối đa là bao nhiêu, và có thể viết unit test để đảm bảo projection không bao giờ vô tình leak field nhạy cảm (như team_metadata hoặc thông tin nội bộ khác). Đây là kỹ thuật nên ưu tiên áp dụng đầu tiên cho mọi loại dữ liệu có cấu trúc trong hệ thống của bạn — chỉ dùng semantic filtering cho phần dữ liệu tự do, phi cấu trúc còn lại.
Technique 3: Summarize-Before-Inject
Có những trường hợp cả hai kỹ thuật trên đều không đủ: thông tin cần thiết cho sub-agent tồn tại, nhưng nó nằm rải rác trong một khối lượng lớn văn bản mà không thể trích xuất gọn theo field cố định, và cũng không thể lọc bằng ngưỡng similarity vì gần như toàn bộ đoạn văn bản đó đều "liên quan" ở mức độ nào đó (ví dụ: một cuộc thảo luận dài 8.000 token giữa nhiều agent trước đó về cách tiếp cận một vấn đề kiến trúc phức tạp).
Trong trường hợp này, kỹ thuật summarize-before-inject (tóm tắt trước khi tiêm vào context) sử dụng một LLM — thường là một model nhỏ, rẻ hơn model chính — để đọc toàn bộ đoạn thông tin dài, và sinh ra một bản tóm tắt có cấu trúc, ngắn gọn, giữ lại đúng các quyết định/kết luận/ràng buộc quan trọng, trước khi đưa vào context của sub-agent.
SUMMARIZE_PROMPT = """You are compressing context for a downstream agent.
The downstream agent's task is: {task_description}
Summarize the following material into at most 150 tokens.
Keep only: decisions made, constraints agreed upon, and open questions
directly relevant to the task above. Discard exploratory discussion,
rejected alternatives, and pleasantries.
Material to summarize:
---
{raw_material}
---
"""
def summarize_before_inject(
raw_material: str,
task_description: str,
cheap_llm_call,
max_tokens: int = 150,
) -> str:
prompt = SUMMARIZE_PROMPT.format(
task_description=task_description,
raw_material=raw_material,
)
# Use a cheaper/faster model here — summarization quality
# requirements are lower than the main reasoning task.
return cheap_llm_call(prompt, max_tokens=max_tokens)
summary = summarize_before_inject(
raw_material=architecture_discussion_transcript, # ~8,000 tokens
task_description="Implement the caching layer as agreed in the discussion.",
cheap_llm_call=call_haiku_model,
)
Cần lưu ý một rủi ro cố hữu của kỹ thuật này: tóm tắt là một phép biến đổi có mất thông tin (lossy). Nếu bản tóm tắt bỏ sót một ràng buộc quan trọng, sub-agent downstream sẽ đưa ra quyết định sai mà không biết mình đang thiếu thông tin. Vì vậy, summarize-before-inject nên được áp dụng có chọn lọc — ưu tiên cho phần nội dung "diễn giải, thảo luận" hơn là cho các con số, tên biến, đường dẫn file chính xác (những thứ này nên được giữ nguyên văn hoặc trích xuất qua schema-based projection).
Mẹo
- Kết hợp cả ba kỹ thuật theo tầng: dùng schema-based projection cho phần dữ liệu có cấu trúc trước, dùng semantic filtering để chọn ra các đoạn văn bản tự do liên quan, rồi mới dùng summarize-before-inject để nén phần văn bản tự do đó nếu vẫn còn quá dài sau khi lọc.
- Với summarize-before-inject, luôn yêu cầu model output theo một cấu trúc rõ ràng (ví dụ: "Decisions:", "Constraints:", "Open questions:") — bản tóm tắt có cấu trúc dễ kiểm tra chất lượng và dễ debug hơn văn xuôi tự do.
- Viết test case "golden context" — một tập các task mẫu kèm context slice kỳ vọng — để phát hiện sớm khi một thay đổi trong scope resolver vô tình làm mất thông tin quan trọng hoặc để leak thông tin không cần thiết.
Scoped Sessions in Practice: Framework-Specific Approaches
Ba framework agentic phổ biến — LangGraph, CrewAI, và AutoGen — đều cung cấp cơ chế riêng để triển khai scoped session, dù cách tiếp cận và thuật ngữ có khác nhau. Hiểu rõ cơ chế native của từng framework giúp bạn tránh phải tự xây dựng lại toàn bộ lớp scope resolver từ đầu.
LangGraph: Using Subgraphs with Isolated State
LangGraph mô hình hóa agentic workflow như một graph (đồ thị) các node, với state (trạng thái) được truyền qua các node theo một schema xác định. Cơ chế subgraph (đồ thị con) cho phép một nhóm node hoạt động với một state schema hoàn toàn riêng, độc lập với state schema của graph cha — đây chính là công cụ tự nhiên nhất để triển khai scoped session trong LangGraph.
Điểm mấu chốt: state của parent graph (đồ thị cha) và state của subgraph không tự động đồng bộ toàn bộ. Bạn phải viết một hàm chuyển đổi (transform function) rõ ràng để lấy ra đúng phần dữ liệu cần thiết từ parent state khi gọi vào subgraph, và một hàm khác để chiếu kết quả từ subgraph trở lại parent state.
from typing import TypedDict, List
from langgraph.graph import StateGraph, END
class ParentState(TypedDict):
user_request: str
full_conversation_history: List[str] # could be 50+ turns
ci_cd_logs: str
deployment_metadata: dict
candidate_file_path: str
candidate_file_content: str
bug_fix_result: str
class BugFixState(TypedDict):
file_path: str
file_content: str
error_summary: str
proposed_patch: str
def enter_bug_fix_subgraph(parent_state: ParentState) -> BugFixState:
"""Scope resolver: project parent state down to the minimal
slice the subgraph actually needs. Conversation history,
CI/CD logs, and deployment metadata are deliberately dropped."""
return BugFixState(
file_path=parent_state["candidate_file_path"],
file_content=parent_state["candidate_file_content"],
error_summary=summarize_error(parent_state["ci_cd_logs"]),
proposed_patch="",
)
def exit_bug_fix_subgraph(sub_state: BugFixState, parent_state: ParentState) -> ParentState:
"""Result projection: only the final patch is written back
to parent state — subgraph's internal reasoning stays isolated."""
parent_state["bug_fix_result"] = sub_state["proposed_patch"]
return parent_state
def propose_patch_node(state: BugFixState) -> BugFixState:
# This node's LLM call only ever sees BugFixState's ~4 fields —
# never the parent's 50-turn conversation history.
patch = call_llm_for_patch(state["file_content"], state["error_summary"])
return {**state, "proposed_patch": patch}
bug_fix_subgraph = StateGraph(BugFixState)
bug_fix_subgraph.add_node("propose_patch", propose_patch_node)
bug_fix_subgraph.set_entry_point("propose_patch")
bug_fix_subgraph.add_edge("propose_patch", END)
compiled_subgraph = bug_fix_subgraph.compile()
def bug_fix_wrapper_node(parent_state: ParentState) -> ParentState:
scoped_input = enter_bug_fix_subgraph(parent_state)
scoped_output = compiled_subgraph.invoke(scoped_input)
return exit_bug_fix_subgraph(scoped_output, parent_state)
parent_graph = StateGraph(ParentState)
parent_graph.add_node("bug_fix", bug_fix_wrapper_node)
Nhờ cấu trúc này, mọi lời gọi LLM bên trong bug_fix_subgraph chỉ tiếp xúc với 4 field của BugFixState — không hề "nhìn thấy" full_conversation_history hay ci_cd_logs thô của parent, dù về mặt logic hai state này tồn tại trong cùng một lượt chạy graph.
CrewAI: Agent-Level Context Scoping
CrewAI xây dựng mô hình xoay quanh khái niệm Agent với role, goal, backstory được định nghĩa hẹp, và Task được gán cho agent cụ thể kèm theo context — danh sách các Task khác mà kết quả của chúng sẽ được đưa vào làm input. Cách scoping tự nhiên nhất trong CrewAI là: (1) thiết kế agent với role càng hẹp càng tốt, và (2) kiểm soát chặt tham số context của từng Task để nó chỉ nhận output từ đúng những Task cần thiết, không phải toàn bộ crew's shared memory.
from crewai import Agent, Task, Crew
file_patch_agent = Agent(
role="Database Query Patch Specialist",
goal="Fix the specific SQL query bug described in the assigned task, nothing more.",
backstory=(
"You are a focused specialist who only edits the exact file and "
"query given to you. You do not investigate unrelated files or "
"propose architectural changes."
),
allow_delegation=False, # prevents scope creep via unbounded delegation
verbose=False,
)
scoped_task = Task(
description=(
"Fix the duplicate-row bug in this query:\n\n"
"{query_snippet}\n\n"
"Error observed: {error_message}\n\n"
"Return only the corrected query and a one-line explanation."
),
expected_output="A corrected SQL query and a one-line explanation.",
agent=file_patch_agent,
# `context` would list other Task objects whose output feeds this
# task — left empty here because this task is self-contained and
# needs no upstream task output at all.
context=[],
)
scoped_task.description = scoped_task.description.format(
query_snippet=extract_query_snippet(incident_record), # ~80 tokens
error_message=incident_record.error_message, # ~40 tokens
)
crew = Crew(agents=[file_patch_agent], tasks=[scoped_task])
result = crew.kickoff()
Một sai lầm phổ biến trong CrewAI là để allow_delegation=True một cách tùy tiện, hoặc thiết kế role/backstory quá rộng ("You are a helpful software engineer who can do anything") — điều này khiến agent có xu hướng "tự mở rộng phạm vi" nhiệm vụ, dẫn tới việc nó tự yêu cầu thêm context không cần thiết, hoặc delegate sang agent khác một cách không kiểm soát, phá vỡ nguyên tắc least context ngay từ bước thiết kế agent.
AutoGen: Using Conversation Termination and Fresh Instances
AutoGen tiếp cận theo hướng hội thoại nhiều tác nhân (multi-agent conversation), nơi các agent trao đổi message qua lại trong cùng một cuộc hội thoại nhóm (group chat). Vì bản chất "hội thoại chung", nguy cơ context bị chia sẻ tràn lan giữa các agent trong AutoGen thường cao hơn so với LangGraph hay CrewAI — nếu không chủ động kiểm soát.
Hai cơ chế chính để thực hiện scoping trong AutoGen là: conversation termination (kết thúc hội thoại đúng lúc) và fresh agent instances (tạo instance agent mới, sạch, cho mỗi phiên làm việc con) thay vì tái sử dụng một agent đã tích lũy lịch sử dài từ các task trước.
from autogen import AssistantAgent, UserProxyAgent
def run_scoped_subtask(task_description: str, minimal_context: dict) -> str:
"""Create a fresh AutoGen agent pair for a single subtask.
No state is carried over from any previous subtask — each call
starts a brand-new, isolated conversation."""
# Fresh instance: this agent has zero conversation history from
# any prior subtask. Its system message is scoped to this one job.
scoped_assistant = AssistantAgent(
name="patch_specialist",
system_message=(
"You fix exactly the bug described below. Do not ask for "
"additional context beyond what is provided. Reply with "
"'TERMINATE' immediately after giving the fix."
),
llm_config={"model": "gpt-4o-mini", "temperature": 0},
)
scoped_user_proxy = UserProxyAgent(
name="task_runner",
human_input_mode="NEVER",
# Termination condition: the moment the assistant signals it's
# done, the conversation stops — no further turns accumulate
# context or burn extra tokens.
is_termination_msg=lambda msg: "TERMINATE" in msg.get("content", ""),
max_consecutive_auto_reply=2, # hard cap as a safety net
code_execution_config=False,
)
prompt = (
f"Task: {task_description}\n\n"
f"File: {minimal_context['file_path']}\n"
f"Content:\n{minimal_context['file_content']}\n\n"
f"Error:\n{minimal_context['error_summary']}"
)
scoped_user_proxy.initiate_chat(scoped_assistant, message=prompt)
# Extract only the final answer — the rest of the transcript
# (including this fresh agent pair) is discarded once this
# function returns. Nothing persists to the next subtask.
return extract_final_reply(scoped_user_proxy, scoped_assistant)
result_1 = run_scoped_subtask("Fix duplicate-row bug", minimal_context=ctx_1)
result_2 = run_scoped_subtask("Fix null-pointer bug", minimal_context=ctx_2)
Điểm cần lưu ý: nếu bạn tái sử dụng cùng một AssistantAgent instance cho nhiều subtask liên tiếp trong AutoGen mà không reset, lịch sử hội thoại của các subtask trước sẽ tích lũy dần trong bộ nhớ của agent đó — đây là nguồn context creep rất phổ biến trong hệ thống dùng AutoGen ở production. Việc chủ động tạo instance mới (hoặc gọi rõ ràng cơ chế reset lịch sử mà AutoGen cung cấp) cho mỗi đơn vị công việc độc lập là cách đơn giản và hiệu quả nhất để đảm bảo scoping.
Mẹo
- Bất kể dùng framework nào, hãy viết một bộ test riêng cho "context boundary" — gọi sub-agent với input cố định, và assert rằng payload thực sự gửi tới LLM API không vượt quá kích thước kỳ vọng. Đừng chỉ tin vào thiết kế logic, hãy đo bằng số token thật.
- Trong LangGraph, tận dụng khả năng subgraph có schema riêng để bắt lỗi ngay ở compile-time nếu bạn vô tình tham chiếu một field không tồn tại trong
BugFixState— đây là một lợi thế kiểu tĩnh (type safety) mà semantic filtering thuần túy không có được. - Trong CrewAI, review lại toàn bộ
backstoryvàrolecủa agent định kỳ — chúng có xu hướng "phình to" theo thời gian khi các thành viên team thêm dần chỉ dẫn, và một backstory dài cũng là một dạng context cố định bị lặp lại ở mọi lần gọi. - Trong AutoGen, luôn đặt
max_consecutive_auto_replylàm lớp bảo vệ cuối cùng, ngay cả khi bạn đã cóis_termination_msg— hai cơ chế nên tồn tại song song để tránh trường hợp agent "quên" phát tín hiệu kết thúc.
The Context Scope Registry Pattern
Khi số lượng loại sub-agent trong hệ thống tăng lên — 5, 10, rồi 20 loại — việc định nghĩa logic scoping rời rạc ở từng nơi gọi sub-agent sẽ nhanh chóng trở thành một nguồn nợ kỹ thuật (technical debt) khó quản lý: không ai biết chắc sub-agent nào đang nhận field gì, thay đổi một schema ở một nơi có thể vô tình phá vỡ context slice ở nơi khác, và không có cách nào audit tổng thể "hệ thống của chúng ta đang gửi bao nhiêu token trung bình cho mỗi loại agent".
Context Scope Registry (registry định nghĩa phạm vi ngữ cảnh) giải quyết vấn đề này bằng cách tập trung hóa toàn bộ định nghĩa scoping vào một nơi duy nhất — một registry ánh xạ từ "loại sub-agent" sang "định nghĩa context slice tương ứng" (schema, nguồn dữ liệu, và các bước biến đổi cần áp dụng).
from dataclasses import dataclass
from typing import Callable, Type, Any
@dataclass
class ScopeDefinition:
"""Declares, in one place, exactly what a given sub-agent type
is allowed to receive — its schema, and the projection function
that builds it from global state."""
agent_type: str
output_schema: Type
projector: Callable[[Any], Any] # global_state -> scoped context
max_tokens_budget: int
description: str
class ContextScopeRegistry:
def __init__(self):
self._registry: dict[str, ScopeDefinition] = {}
def register(self, definition: ScopeDefinition) -> None:
self._registry[definition.agent_type] = definition
def build_scoped_context(self, agent_type: str, global_state: Any) -> Any:
definition = self._registry[agent_type]
scoped_context = definition.projector(global_state)
# Enforce the declared budget — fail loudly rather than
# silently shipping an oversized context slice to the LLM.
actual_tokens = estimate_tokens(scoped_context)
if actual_tokens > definition.max_tokens_budget:
raise ContextBudgetExceeded(
f"{agent_type} scope produced {actual_tokens} tokens, "
f"budget is {definition.max_tokens_budget}"
)
return scoped_context
def audit_report(self) -> list[dict]:
"""One-shot view of every registered scope: useful for
reviewing token budgets across the whole system at once."""
return [
{
"agent_type": d.agent_type,
"max_tokens_budget": d.max_tokens_budget,
"schema_fields": list(d.output_schema.__annotations__.keys()),
}
for d in self._registry.values()
]
registry = ContextScopeRegistry()
registry.register(ScopeDefinition(
agent_type="bug_fix_agent",
output_schema=BugFixAgentContext,
projector=project_for_bug_fix,
max_tokens_budget=300,
description="Fixes a single, well-identified code bug.",
))
registry.register(ScopeDefinition(
agent_type="test_writer_agent",
output_schema=TestWriterContext,
projector=project_for_test_writer,
max_tokens_budget=500,
description="Writes unit tests for a given function or module.",
))
scoped_ctx = registry.build_scoped_context("bug_fix_agent", global_state)
Lợi ích lớn nhất của registry pattern không nằm ở code — mà ở việc nó biến scoping thành một artifact có thể review. Khi thêm một loại sub-agent mới, team có thể review scope definition của nó như review một API contract; khi audit hệ thống, audit_report() cho ra ngay bảng tổng hợp token budget của toàn hệ thống, giúp phát hiện những sub-agent có budget bất thường cao so với độ phức tạp task của nó.
Mẹo
- Đặt
max_tokens_budgetcho mỗi scope definition dựa trên số liệu đo thực tế (percentile 95 của context slice thực tế qua vài trăm lần chạy), không phải một số ước lượng chủ quan — số liệu thực tế thường cao hơn dự đoán ban đầu khá nhiều. - Coi registry là nơi duy nhất được phép tạo context slice — nếu có code gọi sub-agent mà bypass registry (tự build context tay), đó là dấu hiệu scoping đang bị xói mòn (erosion) và cần refactor lại.
- Thêm versioning cho scope definition (ví dụ
bug_fix_agent_v2) khi thay đổi schema, để có thể rollback nhanh nếu phiên bản mới gây regression về chất lượng agent.
Measuring Session Scope Quality
Thiết kế scoped session mà không đo lường là thiết kế trong bóng tối. Có bốn chỉ số (metric) cốt lõi nên được theo dõi liên tục để đánh giá chất lượng scoping trong hệ thống production:
1. Context Reduction Ratio (tỷ lệ giảm context). So sánh kích thước context slice thực tế gửi cho sub-agent với kích thước global state đầy đủ tại thời điểm gọi. Ví dụ: nếu global state có 6.000 token và context slice chỉ 300 token, tỷ lệ giảm là 95%. Theo dõi chỉ số này theo thời gian giúp phát hiện "context creep" — khi tỷ lệ giảm dần thu hẹp lại (ví dụ từ 95% xuống 70%) nghĩa là ai đó đang âm thầm nhồi thêm field vào context slice mà không xem lại tổng thể.
2. Task Success Rate theo mức độ scoping (tỷ lệ hoàn thành task). Đây là chỉ số đối trọng quan trọng nhất với Context Reduction Ratio — vì mục tiêu không phải là giảm token tối đa mà là giảm token mà không làm giảm chất lượng. So sánh tỷ lệ sub-agent hoàn thành task thành công (đúng, không cần hỏi lại, không lỗi) trước và sau khi áp dụng scoping. Nếu tỷ lệ giảm context tăng nhưng tỷ lệ hoàn thành task cũng giảm theo, đó là dấu hiệu rõ ràng của over-scoping (cắt quá tay) — bạn đã lọc mất thông tin cần thiết.
3. Follow-up / Clarification Rate (tỷ lệ agent phải hỏi lại hoặc request thêm context). Nếu sub-agent thường xuyên phải dừng lại để "hỏi thêm thông tin" hoặc trả về kết quả kèm ghi chú "tôi cần biết thêm về X để hoàn thành chính xác", đây là chỉ số trực tiếp cho thấy context slice đang thiếu thông tin quan trọng. Theo dõi chỉ số này theo từng loại sub-agent (từ registry) giúp khoanh vùng chính xác scope definition nào cần được điều chỉnh.
4. Cross-Contamination Rate (tỷ lệ context bị "lẫn" giữa các phiên). Đo tần suất sub-agent tham chiếu tới thông tin không có trong context slice khai báo của nó — thường là dấu hiệu của lỗi triển khai (session boundary bị rò, hoặc registry bị bypass) hơn là vấn đề thiết kế. Có thể phát hiện bằng cách kiểm tra output của sub-agent có nhắc tới thực thể/thuật ngữ nào không xuất hiện trong context slice đầu vào của nó hay không.
Một cách thực tế để tổng hợp các chỉ số này thành một bảng theo dõi:
| Loại sub-agent | Global state (token) | Context slice (token) | Reduction | Success rate | Follow-up rate |
|---|---|---|---|---|---|
| bug_fix_agent | ~6.000 | ~280 | 95,3% | 92% | 4% |
| test_writer_agent | ~6.000 | ~520 | 91,3% | 88% | 9% |
| doc_summary_agent | ~6.000 | ~1.100 | 81,7% | 95% | 2% |
Bảng ví dụ trên minh họa một tình huống thực tế đáng chú ý: test_writer_agent có follow-up rate cao hơn hẳn (9%) dù đã giảm context tới 91,3% — đây là dấu hiệu cho thấy scope definition của nó có thể đang thiếu một field quan trọng (ví dụ: thiếu import path hoặc convention đặt tên test hiện có trong codebase), và cần được điều chỉnh lại thay vì tiếp tục "cắt thêm" để tối ưu token.
Mẹo
- Không tối ưu Context Reduction Ratio một cách độc lập — luôn nhìn nó cùng với Task Success Rate trên cùng một dashboard. Một con số giảm token đẹp mà không kèm dữ liệu chất lượng là con số vô nghĩa, dễ gây ảo tưởng về hiệu quả.
- Thiết lập ngưỡng cảnh báo (alert) khi Follow-up Rate của một loại sub-agent vượt quá một mốc nhất định (ví dụ 8-10%) trong một khoảng thời gian — đây thường là chỉ báo sớm nhất, xuất hiện trước khi vấn đề trở nên nghiêm trọng và ảnh hưởng rõ tới Success Rate.
- Lấy mẫu ngẫu nhiên (random sampling) một số lượng nhỏ session mỗi tuần để review bằng tay (manual review) — các chỉ số định lượng phát hiện xu hướng, nhưng review bằng tay thường phát hiện ra những vấn đề tinh vi hơn (ví dụ: context slice đúng field, nhưng dữ liệu trong field đó đã lỗi thời).
Summary
Scoped agent session là kỹ thuật áp dụng principle of least context vào thiết kế hệ thống multi-agent: mỗi sub-agent chỉ nhận đúng, đủ lượng thông tin cần thiết cho task của nó, thay vì được "tắm" trong toàn bộ global state của hệ thống. Về kiến trúc, điều này đòi hỏi một scope resolver đứng giữa orchestrator và sub-agent, tạo ra context slice tối thiểu, và một cơ chế result projection để đưa kết quả trở lại mà không kéo theo toàn bộ transcript suy luận nội bộ.
Ba kỹ thuật lọc context — semantic relevance filtering (cho dữ liệu phi cấu trúc), schema-based projection (cho dữ liệu có cấu trúc, thường đạt mức giảm 90%+ token), và summarize-before-inject (cho các đoạn văn bản dài không thể trích xuất gọn theo field) — thường được kết hợp theo tầng để xử lý các loại dữ liệu khác nhau trong cùng một hệ thống. Mỗi framework agentic phổ biến cung cấp cơ chế native riêng để triển khai: subgraph với state schema riêng trong LangGraph, agent role hẹp kết hợp kiểm soát context của Task trong CrewAI, và conversation termination cùng fresh instance trong AutoGen.
Khi hệ thống mở rộng, Context Scope Registry Pattern giúp tập trung hóa toàn bộ định nghĩa scoping thành một artifact có thể review, kiểm thử, và audit — thay vì để logic scoping rời rạc, khó kiểm soát ở từng nơi gọi sub-agent. Và cuối cùng, mọi nỗ lực scoping đều cần được đo lường bằng bốn chỉ số song hành: tỷ lệ giảm context, tỷ lệ hoàn thành task, tỷ lệ phải hỏi lại, và tỷ lệ cross-contamination — bởi mục tiêu thật sự không phải là gửi ít token nhất có thể, mà là gửi đúng lượng token cần thiết để agent suy luận chính xác, không thừa, không thiếu.