AI Agent Roadmap: Xây dựng Agent AI Từ Khởi Đầu Chỉ Với API LLM

AI Agent Roadmap: Xây dựng Agent AI Từ Khởi Đầu Chỉ Với API LLM

Chào mừng trở lại với AI Agent Roadmap! Chúng ta đã cùng nhau khám phá AI Agent là gì, hiểu về vòng lặp Agent, cách xây dựng công cụ, đi sâu vào LLM và Transformer, cũng như các khía cạnh kỹ thuật như Tokenization, Cửa sổ ngữ cảnhĐịnh giá theo token. Chúng ta cũng đã tìm hiểu cách quản lý bộ nhớ Agent, sử dụng RAG, và các kỹ thuật prompt nâng cao. Cho đến nay, chúng ta thường nói về các khái niệm hoặc sử dụng các framework cấp cao. Nhưng điều gì xảy ra bên dưới? Làm thế nào chúng ta có thể xây dựng một Agent cơ bản chỉ bằng cách tương tác trực tiếp với API của một mô hình ngôn ngữ lớn (LLM)?

Trong bài viết này, chúng ta sẽ đi sâu vào quá trình xây dựng Agent AI từ đầu, chỉ dựa vào API LLM. Đây là cách tiếp cận “từ khởi đầu” (from scratch) sẽ giúp bạn hiểu rõ hơn các thành phần cốt lõi hoạt động như thế nào và chuẩn bị cho việc xây dựng các Agent phức tạp hơn hoặc sử dụng các framework một cách hiệu quả hơn.

Tại Sao Lại Xây Dựng “Từ Khởi Đầu”?

Có những lý do chính đáng để bạn muốn bắt tay vào xây dựng Agent AI mà không phụ thuộc hoàn toàn vào các thư viện hoặc framework abstraction ban đầu:

  • Hiểu Sâu Sắc Cơ Chế: Việc tự mình quản lý từng bước của vòng lặp Agent, từ việc gửi prompt đến LLM, phân tích kết quả, quyết định hành động và xử lý công cụ, mang lại sự hiểu biết sâu sắc về cách các thành phần tương tác với nhau.
  • Kiểm Soát Tối Đa: Bạn có toàn quyền kiểm soát logic, cách xử lý lỗi, định dạng dữ liệu và tích hợp với các hệ thống khác. Điều này rất quan trọng khi xây dựng các Agent cho các trường hợp sử dụng đặc thù hoặc yêu cầu hiệu năng cao.
  • Tùy Chỉnh Linh Hoạt: Bạn không bị giới hạn bởi các mẫu hoặc cấu trúc được cung cấp bởi framework. Bạn có thể thiết kế Agent của mình theo bất kỳ cách nào phù hợp nhất với nhu cầu của bạn.
  • Giảm Độ Phụ Thuộc: Mặc dù chúng ta vẫn phụ thuộc vào API của nhà cung cấp LLM, nhưng việc xây dựng logic Agent cốt lõi giúp giảm sự phụ thuộc vào các thư viện Agent cụ thể.

Đây là một bước quan trọng trên AI Agent Roadmap của bạn, đặc biệt nếu bạn là một nhà phát triển muốn nắm vững nền tảng.

Các Thành Phần Cốt Lõi (Từ Góc Độ “Scratch”)

Nhắc lại các thành phần cơ bản của một Agent mà chúng ta đã đề cập trong các bài trước, nhưng lần này là cách chúng ta sẽ quản lý chúng:

  1. LLM (LMM API): Bộ não. Chúng ta sẽ tương tác trực tiếp với nó thông qua API (ví dụ: OpenAI API, Gemini API, Anthropic API). Chúng ta gửi cho nó prompt và nhận phản hồi dưới dạng văn bản hoặc JSON.
  2. Prompt: Hướng dẫn cho bộ não. Thay vì dựa vào framework để tạo prompt, chúng ta sẽ tự xây dựng các chuỗi prompt một cách chiến lược để định hướng LLM. Điều này bao gồm việc mô tả nhiệm vụ, cung cấp ngữ cảnh, định nghĩa định dạng đầu ra mong muốn và mô tả các công cụ có sẵn. Các bài về viết prompt hiệu quảcác mô hình prompt engineering sẽ rất hữu ích ở đây.
  3. Bộ nhớ (Memory): Cách Agent duy trì ngữ cảnh qua các lượt tương tác. Khi xây dựng từ đầu, bộ nhớ thường được quản lý bằng cách lưu trữ lịch sử các tin nhắn (user input, LLM response, tool calls, tool outputs) và đưa chúng vào prompt của các lượt gọi API tiếp theo. Các kỹ thuật quản lý bộ nhớ nâng cao như tóm tắt hoặc sử dụng cơ sở dữ liệu vector sẽ cần được triển khai thủ công.
  4. Công cụ (Tools/Functions): Khả năng tương tác với thế giới bên ngoài (tìm kiếm web, gọi API khác, chạy code). Thay vì framework cung cấp sẵn các wrapper công cụ, chúng ta sẽ tự định nghĩa các chức năng (functions) trong code của mình và dạy LLM cách “gọi” chúng thông qua prompt hoặc tính năng Function Calling của API LLM. Kiến thức từ bài Thiết kế Công cụTriển khai Function Calling sẽ là nền tảng.
  5. Vòng lặp Agent (Agent Loop): Logic điều khiển toàn bộ quá trình. Đây là trái tim của Agent khi xây dựng từ đầu. Chúng ta sẽ viết code để thực hiện các bước: nhận input -> chuẩn bị prompt (bao gồm lịch sử/bộ nhớ và mô tả công cụ) -> gọi API LLM -> phân tích kết quả -> nếu LLM yêu cầu sử dụng công cụ, thực thi công cụ đó -> nếu LLM đưa ra câu trả lời cuối cùng, trả về kết quả -> nếu không, lặp lại quá trình. Khái niệm ReAct (Reasoning and Acting) là một mô hình tuyệt vời để triển khai vòng lặp này.

Tương Tác Với API LLM

Bước đầu tiên là biết cách nói chuyện với LLM. Hầu hết các nhà cung cấp LLM lớn (OpenAI, Google Gemini, Anthropic Claude, v.v.) đều cung cấp API HTTP hoặc SDK (thư viện phần mềm) cho các ngôn ngữ lập trình phổ biến như Python, Node.js. Chúng ta sẽ sử dụng Python SDK làm ví dụ.

import os
# import requests # Bạn có thể dùng requests nếu không muốn dùng SDK
from openai import OpenAI # Ví dụ với OpenAI

# Lấy API Key từ biến môi trường
# Đảm bảo bạn đã cài đặt biến môi trường OPENAI_API_KEY
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
    raise ValueError("Biến môi trường OPENAI_API_KEY chưa được cài đặt.")

client = OpenAI(api_key=api_key)

def call_llm(messages, model="gpt-4o-mini", temperature=0.7):
    """
    Gọi API Chat Completions của OpenAI.

    Args:
        messages (list): Danh sách các tin nhắn trong cuộc hội thoại.
                         Mỗi tin nhắn là một dict có cấu trúc {'role': ..., 'content': ...}
                         Các role phổ biến: 'system', 'user', 'assistant', 'tool'.
        model (str): Tên model LLM.
        temperature (float): Tham số kiểm soát tính ngẫu nhiên của output (0.0 đến 2.0).

    Returns:
        str: Nội dung text từ phản hồi của LLM, hoặc None nếu có lỗi.
    """
    try:
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            temperature=temperature
        )
        # Trích xuất nội dung text từ phản hồi
        # Kiểm tra xem có choices nào không và tin nhắn có nội dung không
        if response.choices and response.choices[0].message.content:
            return response.choices[0].message.content
        elif response.choices and response.choices[0].message.tool_calls:
             # Xử lý trường hợp LLM gọi công cụ (chúng ta sẽ nói về cái này sau)
             return response.choices[0].message.tool_calls
        else:
            print("LLM returned an empty response.")
            return None
    except Exception as e:
        print(f"Error calling LLM API: {e}")
        return None

# Ví dụ sử dụng hàm call_llm
if __name__ == "__main__":
    conversation_history = [
        {"role": "system", "content": "Bạn là một trợ lý hữu ích."},
        {"role": "user", "content": "Thủ đô của Pháp là gì?"}
    ]

    llm_response = call_llm(conversation_history)

    if llm_response:
        print("LLM Response:")
        print(llm_response)

    # Thêm phản hồi của assistant vào lịch sử để tiếp tục cuộc hội thoại
    if llm_response:
        conversation_history.append({"role": "assistant", "content": llm_response})
        conversation_history.append({"role": "user", "content": "Nó nằm ở châu lục nào?"})

        llm_response_2 = call_llm(conversation_history)
        if llm_response_2:
            print("\nLLM Response (lượt 2):")
            print(llm_response_2)

Đoạn code trên minh họa cách cơ bản nhất để tương tác với API LLM: chuẩn bị danh sách `messages` (đại diện cho lịch sử cuộc hội thoại và prompt hệ thống/người dùng) và gửi đi. Phản hồi là nội dung text mà LLM tạo ra. Chú ý rằng danh sách `messages` chính là cách chúng ta “truyền bộ nhớ” cho LLM trong cách tiếp cận từ đầu.

Triển Khai Vòng Lặp Agent Cơ Bản

Bây giờ chúng ta có thể gọi LLM, hãy xây dựng logic điều khiển nó – vòng lặp Agent. Chúng ta sẽ triển khai một vòng lặp đơn giản:

  1. Nhận yêu cầu từ người dùng.
  2. Chuẩn bị prompt ban đầu (bao gồm hướng dẫn cho Agent, mô tả công cụ).
  3. Gửi prompt đến LLM.
  4. Phân tích phản hồi của LLM để xem nó muốn làm gì (đưa ra câu trả lời cuối cùng hay sử dụng công cụ).
  5. Nếu sử dụng công cụ, xác định công cụ nào và đối số.
  6. Thực thi công cụ đã xác định.
  7. Nhận kết quả từ công cụ.
  8. Thêm kết quả công cụ vào lịch sử cuộc hội thoại.
  9. Quay lại bước 3 (gửi lịch sử cập nhật đến LLM) cho đến khi LLM đưa ra câu trả lời cuối cùng.

Đây là cấu trúc ReAct cơ bản, được thực hiện thủ công. Chúng ta cần các thành phần bổ sung:

  • Một hàm hoặc lớp để quản lý lịch sử `messages`.
  • Một danh sách các công cụ có sẵn (ví dụ: Python functions).
  • Một cơ chế để dạy LLM về các công cụ này (thông qua prompt hoặc Function Calling).
  • Logic để phân tích phản hồi của LLM và gọi đúng công cụ.

Dạy LLM về Công cụ

Có hai cách chính để làm điều này khi xây dựng từ đầu:

  1. Prompt-based (ReAct style): Mô tả các công cụ và định dạng output mong muốn (Suy nghĩ -> Hành động -> Quan sát) trong prompt hệ thống. LLM sẽ cố gắng tuân theo định dạng này. Bạn cần phân tích chuỗi văn bản phản hồi của LLM để tìm các mẫu như `Action: [tool_name]`, `Action Input: [tool_args]`.
  2. Function Calling (API feature): Sử dụng tính năng Function Calling được cung cấp bởi một số API LLM (như OpenAI, Gemini). Bạn định nghĩa cấu trúc JSON của các công cụ và đưa vào yêu cầu API. LLM sẽ phản hồi bằng một đối tượng JSON mô tả công cụ cần gọi và đối số, thay vì tạo ra văn bản tự do. Cách này đáng tin cậy hơn cho việc phân tích, nhưng phụ thuộc vào tính năng cụ thể của API. Chúng ta đã thảo luận về điều này trong bài Triển khai Function Calling.

Với mục tiêu “từ khởi đầu”, ban đầu chúng ta có thể tập trung vào cách tiếp cận Prompt-based để hiểu rõ cơ chế phân tích output LLM. Sau đó, chuyển sang Function Calling để cải thiện độ tin cậy.

Hãy xem xét ví dụ Prompt-based:

SYSTEM_PROMPT_TEMPLATE = """
Bạn là một trợ lý AI thông minh. Bạn có thể trả lời các câu hỏi của người dùng và sử dụng các công cụ sau nếu cần thiết.
Hãy suy nghĩ từng bước trước khi hành động. Luôn tuân thủ định dạng đầu ra sau:

Thought: Suy nghĩ của bạn về vấn đề và bước tiếp theo cần thực hiện.
Action: Tên của công cụ cần gọi, hoặc NONE nếu bạn đã có câu trả lời cuối cùng.
Action Input: Đối số cho công cụ (chuỗi JSON), hoặc NONE nếu Action là NONE.
Observation: Kết quả của công cụ (chỉ sau khi Action đã được thực thi bởi hệ thống).
... (lặp lại Thought/Action/Action Input/Observation nếu cần sử dụng nhiều công cụ)
Thought: Suy nghĩ cuối cùng trước khi trả lời người dùng.
Action: NONE
Action Input: NONE
Final Answer: Câu trả lời cuối cùng cho người dùng.

Các công cụ bạn có thể sử dụng:
{tools_description}

Bắt đầu!
"""

TOOL_DESCRIPTION_TEMPLATE = """
- {tool_name}: {tool_description}
  Định dạng đối số: {tool_args_format}
"""

# Ví dụ về một công cụ
def search_web(query: str) -> str:
    """Tìm kiếm thông tin trên web"""
    print(f"DEBUG: Calling search_web with query: '{query}'")
    # Đây chỉ là mock function, bạn sẽ thay thế bằng API tìm kiếm thực tế
    # Ví dụ tích hợp công cụ tìm kiếm web đã thảo luận ở bài:
    # https://tuyendung.evotek.vn/ai-agent-roadmap-cong-cu-tim-kiem-web-repl-va-api-hoat-dong-trong-ai-agent-nhu-the-nao/
    if "thời tiết hà nội" in query.lower():
        return "Thời tiết Hà Nội hôm nay nắng, nhiệt độ 30 độ C."
    elif "dân số việt nam" in query.lower():
         return "Dân số Việt Nam năm 2023 khoảng 100 triệu người."
    else:
         return f"Không tìm thấy thông tin cho '{query}'."

TOOLS = {
    "search_web": {
        "function": search_web,
        "description": "Tìm kiếm thông tin trên web về một chủ đề cụ thể.",
        "args_format": '{"query": "chuỗi tìm kiếm"}'
    }
    # Thêm các công cụ khác tại đây
}

def get_tools_description(tools_dict):
    description = ""
    for name, tool_info in tools_dict.items():
        description += TOOL_DESCRIPTION_TEMPLATE.format(
            tool_name=name,
            tool_description=tool_info["description"],
            tool_args_format=tool_info["args_format"]
        )
    return description.strip()

def format_prompt(messages, available_tools):
    """Chuẩn bị prompt đầy đủ cho LLM"""
    tools_desc = get_tools_description(available_tools)
    system_prompt = SYSTEM_PROMPT_TEMPLATE.format(tools_description=tools_desc)

    # Thêm prompt hệ thống vào đầu nếu chưa có
    if not messages or messages[0]["role"] != "system":
         messages.insert(0, {"role": "system", "content": system_prompt})
    else:
         # Cập nhật prompt hệ thống nếu cần (ví dụ: mô tả công cụ thay đổi)
         messages[0] = {"role": "system", "content": system_prompt}

    return messages

Triển khai Vòng lặp (Prompt-based Parsing)

Phần phức tạp nhất khi làm từ đầu là phân tích phản hồi của LLM để xác định hành động và đối số. Chúng ta cần viết code để tìm kiếm các mẫu `Thought:`, `Action:`, `Action Input:`.

import json
import re

def parse_llm_output_react(output: str):
    """
    Phân tích output của LLM theo định dạng ReAct (Prompt-based).

    Args:
        output (str): Chuỗi output từ LLM.

    Returns:
        dict: {'thought': ..., 'action': ..., 'action_input': ..., 'final_answer': ...}
              Các trường có thể là None nếu không được tìm thấy.
    """
    parsed_data = {
        'thought': None,
        'action': None,
        'action_input': None,
        'final_answer': None
    }

    # Tìm Thought cuối cùng
    thought_match = re.findall(r"Thought: (.*?)\n", output, re.DOTALL)
    if thought_match:
        parsed_data['thought'] = thought_match[-1].strip()

    # Tìm Action và Action Input cuối cùng
    action_match = re.search(r"Action: (.*?)\n", output, re.DOTALL)
    if action_match:
        parsed_data['action'] = action_match.group(1).strip()

        if parsed_data['action'] and parsed_data['action'].upper() != 'NONE':
            input_match = re.search(r"Action Input: (.*?)\n", output, re.DOTALL)
            if input_match:
                # Cố gắng phân tích Action Input dưới dạng JSON
                action_input_str = input_match.group(1).strip()
                try:
                    # LLM có thể trả về JSON không hợp lệ hoặc chỉ là chuỗi.
                    # Cố gắng parse JSON nếu trông giống JSON object/array
                    if action_input_str.startswith('{') or action_input_str.startswith('['):
                        parsed_data['action_input'] = json.loads(action_input_str)
                    else:
                         # Nếu không giống JSON, coi là chuỗi đơn giản
                         parsed_data['action_input'] = action_input_str
                except json.JSONDecodeError:
                    print(f"Warning: Could not parse Action Input as JSON: {action_input_str}")
                    parsed_data['action_input'] = action_input_str # Lưu lại chuỗi gốc nếu parse lỗi

    # Tìm Final Answer
    final_answer_match = re.search(r"Final Answer: (.*?)$", output, re.DOTALL)
    if final_answer_match:
        parsed_data['final_answer'] = final_answer_match.group(1).strip()

    return parsed_data


def run_agent(user_query: str, available_tools: dict, max_steps=5):
    """
    Chạy vòng lặp Agent cơ bản.

    Args:
        user_query (str): Câu hỏi hoặc yêu cầu của người dùng.
        available_tools (dict): Các công cụ có sẵn.
        max_steps (int): Số bước tối đa trong vòng lặp để tránh lặp vô hạn.

    Returns:
        str: Câu trả lời cuối cùng của Agent.
    """
    # Khởi tạo lịch sử cuộc hội thoại
    # Bắt đầu với user query
    conversation_history = [{"role": "user", "content": user_query}]
    # Prompt hệ thống sẽ được thêm vào bởi format_prompt ở bước đầu tiên

    steps_count = 0
    final_answer = None

    print(f"User: {user_query}")

    while steps_count < max_steps:
        steps_count += 1
        print(f"\n--- Step {steps_count} ---")

        # 1. Chuẩn bị prompt đầy đủ (bao gồm lịch sử và mô tả công cụ)
        prompt_messages = format_prompt(conversation_history.copy(), available_tools) # Dùng copy để không sửa đổi history gốc khi thêm system prompt tạm thời

        # 2. Gọi LLM
        print("Calling LLM...")
        llm_output = call_llm(prompt_messages, model="gpt-4o-mini") # Chọn model phù hợp

        if not llm_output:
            print("LLM failed to return output.")
            final_answer = "Sorry, I encountered an error processing your request."
            break

        # Thêm output LLM vào lịch sử (vai trò assistant)
        # Lưu ý: Trong prompt-based ReAct, output LLM là một chuỗi chứa Thought, Action, v.v.
        # Chúng ta lưu nó như một tin nhắn của assistant
        conversation_history.append({"role": "assistant", "content": llm_output})
        print(f"LLM Output:\n{llm_output}")

        # 3. Phân tích phản hồi của LLM
        parsed_output = parse_llm_output_react(llm_output)

        # 4. Kiểm tra Action
        action = parsed_output.get('action')
        action_input = parsed_output.get('action_input')
        final_answer = parsed_output.get('final_answer')
        thought = parsed_output.get('thought') # Có thể in thought để debug

        if final_answer:
            print("\nAgent decided on Final Answer.")
            break # LLM đã đưa ra câu trả lời cuối cùng, kết thúc vòng lặp

        if action and action.upper() != 'NONE' and action_input is not None:
            print(f"\nAgent decided to use tool: {action} with input: {action_input}")
            # 5. Xác định và thực thi công cụ
            tool_info = available_tools.get(action)
            if tool_info:
                tool_function = tool_info["function"]
                try:
                    # Đối số có thể cần được xử lý tùy thuộc vào cách parse action_input
                    # Nếu parse_llm_output_react trả về dict, có thể truyền trực tiếp:
                    if isinstance(action_input, dict):
                         tool_output = tool_function(**action_input) # Sử dụng ** để truyền dict làm keyword args
                    # Nếu trả về chuỗi, truyền chuỗi:
                    elif isinstance(action_input, str):
                         # Cần biết công cụ mong đợi kiểu gì. Ví dụ search_web mong đợi str
                         tool_output = tool_function(action_input)
                    else:
                         tool_output = f"Error: Unsupported action_input type: {type(action_input)}"
                         print(tool_output)

                except Exception as e:
                    tool_output = f"Error executing tool {action}: {e}"
                    print(tool_output)
            else:
                tool_output = f"Error: Tool '{action}' not found."
                print(tool_output)

            # 6. Nhận kết quả từ công cụ (đã làm trong bước 5)
            # 7. Thêm kết quả công cụ vào lịch sử (trong prompt-based ReAct, Observation là phần của assistant output)
            # Chúng ta cần thêm một message mới để LLM biết kết quả của hành động trước đó.
            # Role 'tool' hoặc 'user' có thể được sử dụng, nhưng 'user' với nội dung bắt đầu bằng "Observation:" phổ biến trong ReAct prompt-based.
            # Hoặc thêm vào message 'assistant' trước đó nếu có thể phân tích chính xác vị trí.
            # Cách đơn giản nhất là thêm một tin nhắn 'user' mới mô phỏng Observation.
            # conversation_history.append({"role": "user", "content": f"Observation: {tool_output}"}) # Cách 1
            # Cách 2: Thêm vào cuối message assistant trước đó. Phức tạp khi phân tích.
            # Cách 3: Sử dụng role 'tool' nếu API hỗ trợ và chúng ta đang mô phỏng Function Calling style trong prompt
            # Với prompt-based ReAct thuần túy, chúng ta chỉ cần gửi lại lịch sử, LLM sẽ tạo ra Observation tiếp theo.
            # Tuy nhiên, để Agent "nhìn thấy" kết quả công cụ, chúng ta cần đưa nó vào input cho lượt gọi LLM tiếp theo.
            # Cách thường làm là thêm nó như một message mới có role phù hợp.
            # Nếu dùng OpenAI/Function Calling, role là 'tool'. Nếu prompt-based thuần, có thể dùng 'user' với định dạng Observation.
            # Với cách tiếp cận "scratch" mô phỏng ReAct prompt-based, ta sẽ thêm nó vào lịch sử để LLM đọc được.
            # Có thể dùng role 'tool' ngay cả khi không dùng Function Calling để phân biệt
            conversation_history.append({"role": "tool", "content": tool_output}) # Sử dụng role 'tool' để rõ ràng hơn về nguồn gốc thông tin

        else:
            # Agent không sử dụng công cụ và cũng không có Final Answer
            # Có thể do LLM không tuân thủ định dạng hoặc bị kẹt
            print("Agent did not choose a tool or provide a final answer.")
            # Có thể thêm logic xử lý lỗi hoặc thử lại ở đây
            if not final_answer: # Nếu không có final answer sau khi vòng lặp kết thúc
                 final_answer = "Sorry, I couldn't complete the task."
            break

    return final_answer

# Ví dụ chạy Agent
if __name__ == "__main__":
    # Đảm bảo TOOLS đã được định nghĩa ở trên
    final_result = run_agent("Thời tiết Hà Nội hôm nay thế nào?", available_tools=TOOLS)
    print(f"\n--- Final Result ---")
    print(final_result)

    print("\n=====================\n")

    final_result_2 = run_agent("Dân số Việt Nam là bao nhiêu?", available_tools=TOOLS)
    print(f"\n--- Final Result ---")
    print(final_result_2)

Đoạn code trên mô phỏng một vòng lặp Agent đơn giản. Nó gọi LLM, phân tích output để tìm hành động (sử dụng công cụ hay trả lời), thực thi công cụ nếu có, và thêm kết quả vào lịch sử trước khi gọi LLM lại. Đây là nền tảng của rất nhiều Agent phức tạp hơn.

Đối với cách tiếp cận Function Calling, việc phân tích output của LLM sẽ dễ dàng hơn nhiều vì API trả về cấu trúc JSON đã định dạng. Bạn chỉ cần kiểm tra xem phản hồi có trường `tool_calls` hay không. Nếu có, trích xuất tên và đối số từ đó.

# Ví dụ parse output Function Calling (giả định output từ OpenAI API)
def parse_llm_output_function_calling(response_message):
    """
    Phân tích output của LLM theo định dạng Function Calling.

    Args:
        response_message (openai.types.chat.chat_completion_message.ChatCompletionMessage): Đối tượng message từ LLM.

    Returns:
        dict: {'tool_calls': [...], 'final_answer': ...}
              'tool_calls' là danh sách các tool_call objects.
              'final_answer' là nội dung text nếu có.
    """
    parsed_data = {
        'tool_calls': None,
        'final_answer': None
    }

    if response_message.tool_calls:
        parsed_data['tool_calls'] = response_message.tool_calls # Đây là danh sách tool_call objects

    if response_message.content:
        parsed_data['final_answer'] = response_message.content # Nội dung text có thể xuất hiện cùng tool_calls

    return parsed_data

# Khi gọi LLM với Function Calling, bạn sẽ thêm tham số `tools` và `tool_choice`
# client.chat.completions.create(
#     model=model,
#     messages=messages,
#     tools=[{
#         "type": "function",
#         "function": {
#             "name": "search_web",
#             "description": "Tìm kiếm thông tin trên web về một chủ đề cụ thể.",
#             "parameters": {
#                 "type": "object",
#                 "properties": {
#                     "query": {"type": "string", "description": "Chuỗi tìm kiếm"}
#                 },
#                 "required": ["query"],
#             },
#         },
#     }],
#     tool_choice="auto" # hoặc "none", or {"type": "function", "function": {"name": "my_tool"}}
# )

# Sau đó, vòng lặp run_agent sẽ kiểm tra parsed_output['tool_calls']
# Thay vì phân tích chuỗi text phức tạp.
# Và khi thực thi tool, output sẽ được thêm vào lịch sử với role 'tool' và tool_call_id tương ứng.
# conversation_history.append({
#    "role": "tool",
#    "tool_call_id": tool_call.id, # ID của tool_call mà output này phản hồi
#    "content": tool_output
# })

So Sánh Hai Cách Tiếp Cận Công Cụ

Việc lựa chọn giữa Prompt-based parsing và Function Calling (nếu có) là một quyết định quan trọng khi xây dựng từ đầu. Dưới đây là bảng so sánh nhanh:

Đặc điểm Prompt-based Parsing (ReAct thuần) Function Calling (Tính năng API)
Cách LLM biết về công cụ Mô tả trong prompt hệ thống Định nghĩa cấu trúc JSON trong yêu cầu API
Cách Agent phân tích output Phân tích chuỗi văn bản phức tạp bằng regex hoặc parsing logic tùy chỉnh Parse cấu trúc JSON được định dạng bởi API
Độ tin cậy của output Có thể không nhất quán, dễ bị LLM “ảo giác” sai định dạng Đáng tin cậy hơn, được API đảm bảo cấu trúc (tùy thuộc vào chất lượng LLM)
Độ phức tạp triển khai parsing Cao (cần logic phân tích chuỗi mạnh mẽ và xử lý lỗi) Thấp (parse JSON tiêu chuẩn)
Độ phụ thuộc vào API Thấp (chỉ cần API trả về text) Cao (phụ thuộc vào tính năng Function Calling cụ thể của API)
Linh hoạt định dạng output Cao (bạn định nghĩa format trong prompt) Thấp (phụ thuộc vào format JSON của API)

Đối với người mới bắt đầu xây dựng từ đầu, việc thử nghiệm cả hai cách là hữu ích để hiểu rõ ưu nhược điểm. Bắt đầu với Prompt-based giúp bạn rèn luyện kỹ năng prompt engineering và phân tích output LLM. Sau đó, chuyển sang Function Calling để thấy sự hiệu quả và độ tin cậy mà các tính năng API mang lại.

Thử Thách và Cân Nhắc

Xây dựng Agent từ đầu mang lại sự linh hoạt nhưng cũng đi kèm với nhiều thách thức:

  • Phân tích output LLM: Đây là điểm yếu lớn nhất của cách tiếp cận Prompt-based. LLM có thể không tuân thủ chính xác định dạng bạn đưa ra, dẫn đến lỗi parsing và Agent không hoạt động đúng.
  • Xử lý lỗi: Bạn phải tự xử lý tất cả các loại lỗi: lỗi API LLM, lỗi khi thực thi công cụ, lỗi parsing output, lỗi trong logic vòng lặp.
  • Quản lý Bộ nhớ: Với các cuộc hội thoại dài, lịch sử `messages` sẽ vượt quá giới hạn cửa sổ ngữ cảnh của LLM (Xem bài về Cửa sổ ngữ cảnh). Bạn cần triển khai các chiến lược quản lý bộ nhớ như nén, tóm tắt hoặc sử dụng Cơ sở dữ liệu VectorRAG.
  • Độ trễ và Chi phí: Mỗi bước trong vòng lặp Agent thường yêu cầu một lượt gọi API LLM. Điều này có thể gây ra độ trễ đáng kể và tăng chi phí nhanh chóng, đặc biệt với các model lớn và các tác vụ phức tạp yêu cầu nhiều bước (Xem bài về Định giá dựa trên token).
  • Độ phức tạp tăng dần: Khi Agent của bạn cần nhiều công cụ hơn, logic phân tích, quản lý state và xử lý lỗi sẽ trở nên phức tạp hơn rất nhiều.

Khi Nào Nên Chuyển Sang Framework?

Việc xây dựng từ đầu là tuyệt vời để học hỏi và kiểm soát. Tuy nhiên, khi dự án của bạn phát triển, bạn sẽ nhận thấy mình liên tục giải quyết cùng các vấn đề: quản lý lịch sử chat, định nghĩa và gọi công cụ một cách đáng tin cậy, triển khai các chiến lược bộ nhớ, theo dõi và gỡ lỗi vòng lặp Agent. Đây chính là lúc các framework phát triển Agent như LangChain, LlamaIndex, hay CrewAI tỏa sáng.

Các framework này cung cấp các abstraction và component đã được xây dựng sẵn cho:

  • Kết nối với nhiều loại LLM và Embedding.
  • Định nghĩa và quản lý công cụ (Tools/Agents).
  • Các loại bộ nhớ khác nhau (Memory).
  • Các chuỗi logic (Chains) và vòng lặp Agent (Agents).
  • Tích hợp với các hệ thống khác (Vector Stores, Databases, APIs).
  • Kiểm thử, đánh giá, và giám sát Agent.

Nếu mục tiêu của bạn là xây dựng một ứng dụng Agent mạnh mẽ, có thể mở rộng và đáng tin cậy một cách nhanh chóng, thì việc sử dụng một framework là lựa chọn thực tế. Tuy nhiên, nền tảng kiến thức bạn có được từ việc xây dựng Agent từ đầu sẽ giúp bạn sử dụng các framework này một cách hiệu quả hơn rất nhiều, hiểu được điều gì đang diễn ra “dưới mui xe”.

Kết Luận

Xây dựng Agent AI từ khởi đầu chỉ với API LLM là một trải nghiệm học tập vô giá. Nó buộc bạn phải đối mặt trực tiếp với các thách thức cốt lõi của kiến trúc Agent: làm thế nào để một mô hình ngôn ngữ chỉ có khả năng sinh văn bản hoặc gọi hàm có thể thực hiện các tác vụ phức tạp, đa bước bằng cách sử dụng công cụ và duy trì trạng thái.

Bạn đã thấy cách chúng ta có thể tái tạo lại vòng lặp ReAct cơ bản chỉ bằng cách chuẩn bị prompt cẩn thận, gọi API LLM và phân tích phản hồi của nó. Bạn cũng đã thấy sự khác biệt giữa cách tiếp cận Prompt-based và sử dụng tính năng Function Calling của API để làm cho việc gọi công cụ đáng tin cậy hơn.

Trong các bài viết tiếp theo của AI Agent Roadmap, chúng ta sẽ tiếp tục xây dựng dựa trên những nền tảng này, khám phá các kiến trúc Agent nâng cao hơn (Planner-Executor, Tree-of-Thought) và cách các framework giúp đơn giản hóa quá trình phát triển. Nhưng bây giờ, hãy thử nghiệm việc xây dựng một Agent đơn giản của riêng bạn bằng cách tương tác trực tiếp với API LLM. Đó là cách tốt nhất để củng cố kiến thức của bạn!

Hẹn gặp lại trong bài viết tiếp theo!

Chỉ mục