Harness Academy
Tài liệu gốc 15 22 phútTiếng Việt + English
docs/en/s12-gateway-architecture.mdXem bản gốc
Ngôn ngữ bài đọc
Bản dịch tiếng Việt

Bản dịch đầy đủ theo đúng cấu trúc tài liệu; code, liên kết và định danh kỹ thuật được giữ nguyên.

s12: Kiến trúc cổng

s00 > s01 > s02 > s03 > s04 > s05 > s06 > s07 > s08 > s09 > s10 > s11 > [ s12 ] > s13 > s14 > s15 > s16 > s17 > s18 > s19 > s20 > s21 > s22 > s23 > s24 > s25 > s26 > s27

CLI là cuộc trò chuyện trực tiếp giữa bạn và agent. Cổng cho phép cùng một subagentc vụ người dùng trên nhiều nền tảng cùng một lúc. Vòng lặp cốt lõi không thay đổi một dòng nào -- điều thay đổi là nơi gửi thư và nơi gửi thư trả lời.

Định tuyến tin nhắn đa nền tảng

Chương này giải quyết vấn đề gì

Bởi s11, bạn đã có một hệ thống agent duy nhất đầy đủ chức năng. Nhưng nó chỉ có một điểm vào: CLI -- bạn nhập một tin nhắn vào terminal, nhân viên sẽ trả lời.

Bây giờ bạn muốn agent này cũng hoạt động trong WeCom (Enterprise WeChat). Bạn làm điều đó như thế nào?

Bắt đầu với cách triển khai đơn giản nhất có thể

Đặt kiến trúc sang một bên, hãy viết "bot WeChat" đơn giản nhất:

# The simplest version: connect to WeChat directly, call the agent on every message

import aiohttp, asyncio

async def main():
    # 1. Connect to WeChat
    ws = await aiohttp.ClientSession().ws_connect("wss://openws.work.weixin.qq.com")
    await ws.send_json({
        "cmd": "aibot_subscribe",
        "body": {"bot_id": "xxx", "secret": "yyy"},
    })

    # 2. Infinite loop: receive message -> call agent -> send reply
    while True:
        msg = await ws.receive_json()
        if msg["cmd"] != "aibot_msg_callback":
            continue

        body = msg["body"]
        user_text = body["text"]["content"]
        chat_id = body["chatid"]

        # 3. Call the core loop from s01 (same function the CLI uses)
        agent = AIAgent(model="anthropic/claude-sonnet-4")
        response = agent.run_conversation(user_text)

        # 4. Send the reply back to WeChat
        await ws.send_json({
            "cmd": "aibot_send_msg",
            "body": {
                "chatid": chat_id,
                "msgtype": "markdown",
                "markdown": {"content": response},
            },
        })

40 dòng mã và nó hoạt động. Nhưng nó có ba vấn đề chết người.

Vấn đề 1: Không có bộ nhớ -- mọi tin nhắn đều bắt đầu một cuộc trò chuyện mới

Đoạn mã trên chỉ chuyển tin nhắn hiện tại tới run_conversation. Người đại diện không biết những gì đã được nói trước đó.

Bạn nói "dịch xin chào" trong WeChat và nhân viên sẽ trả lời bằng bản dịch. Sau đó, bạn nói "bây giờ dịch thế giới" và người đại diện không biết bạn đang dịch -- bởi vì nó không có lịch sử.

Giải pháp: Lưu trữ lịch sử trò chuyện của từng người dùng và chuyển nó vào vòng lặp trong các cuộc gọi tiếp theo.

Nhưng đây là câu hỏi quan trọng: làm sao bạn biết "tin nhắn nào thuộc cùng một cuộc trò chuyện"?

Nếu Zhang San và Li Si đều trò chuyện đồng thời với người đại diện trên WeChat, tin nhắn của họ không thể bị trộn lẫn với nhau. "Giúp tôi viết mã" của Zhang San và "thời tiết hôm nay thế nào" của Li Si là hai cuộc trò chuyện hoàn toàn độc lập.

Bạn cần mã định danh để phân biệt các cuộc trò chuyện khác nhau. agent Hermes gọi nó là khóa phiên -- một chuỗi duy nhất bao gồm nền tảng + ID trò chuyện + ID người dùng:

# Private chat: platform + chat type + user ID
"agent:main:wecom:dm:zhangsan"

# Group chat: platform + chat type + group ID + user ID (per-user isolation within groups)
"agent:main:wecom:group:grp_001:zhangsan"
"agent:main:wecom:group:grp_001:lisi"

Zhang San và Li Si ở cùng một nhóm, nhưng khóa phiên của họ khác nhau nên mỗi người có lịch sử trò chuyện độc lập.

Với tính năng quản lý phiên được thêm vào, mã sẽ trở thành:

sessions = {}  # session_key -> message history list

while True:
    msg = await ws.receive_json()
    body = msg["body"]

    # Generate session key
    user_id = body["from"]["userid"]
    chat_id = body["chatid"]
    chat_type = "dm" if body["chattype"] == "single" else "group"

    if chat_type == "dm":
        session_key = f"agent:main:wecom:dm:{chat_id}"
    else:
        session_key = f"agent:main:wecom:group:{chat_id}:{user_id}"

    # Retrieve this user's history; create empty list if none exists
    history = sessions.setdefault(session_key, [])

    # Append the current message
    history.append({"role": "user", "content": body["text"]["content"]})

    # Create agent with full history
    agent = AIAgent(model="...", conversation_history=history)
    response = agent.run_conversation(body["text"]["content"])

    # The agent internally appends the reply to history

    # Send back to WeChat
    await ws.send_json(...)

Bây giờ agent có bộ nhớ. Nhưng vẫn còn hai vấn đề nữa.

Vấn đề 2: Chỉ kết nối với WeChat -- cần thay đổi bao nhiêu mã để thêm Telegram?

Sản phẩm cho biết: "Chúng tôi cũng có thể kết nối với Telegram được không?"

Bạn nhìn vào API của Telegram - đó không phải là WebSocket, đó là cuộc thăm dò dài HTTP. Định dạng tin nhắn hoàn toàn khác. ID người dùng được gọi from.id (không phải from.userid), ID trò chuyện được gọi chat.id (không phải chatid).

Nếu bạn thêm các nhánh if-else trực tiếp vào vòng lặp while:

# Don't do this
while True:
    if platform == "wecom":
        msg = await ws.receive_json()
        user_id = msg["body"]["from"]["userid"]
        text = msg["body"]["text"]["content"]
        chat_id = msg["body"]["chatid"]
    elif platform == "telegram":
        update = await telegram_get_updates()
        user_id = str(update["message"]["from"]["id"])
        text = update["message"]["text"]
        chat_id = str(update["message"]["chat"]["id"])
    elif platform == "discord":
        ...  # Yet another completely different format

Mỗi nền tảng mới đều tạo ra vòng lặp. 17 nền tảng có nghĩa là 17 chi nhánh. Và logic về "cách kết nối", "cách nhận tin nhắn" và "cách gửi câu trả lời" đều rối rắm với nhau.

Giải pháp: Hãy để mỗi nền tảng xử lý "bản dịch" riêng, sau đó chuyển sang định dạng thống nhất.

Đây là nơi bộ điều hợp nền tảng xuất phát. Mỗi bộ chuyển đổi thực hiện hai việc:

  1. Nhận tin nhắn nền tảng -> dịch sang định dạng thống nhất (inbound)
  2. Nhận văn bản trả lời -> dịch sang định dạng nền tảng và gửi (gửi đi)

Định dạng thống nhất trong Hermes Agent được gọi là MessageEvent:

@dataclass
class MessageEvent:
    message_id: str        # "msg_001"
    text: str              # "Check the weather for me"
    source: SessionSource  # Where it came from (platform, chat ID, user ID)
    message_type: str      # "text", "photo", "voice", ...
@dataclass
class SessionSource:
    platform: str    # "wecom", "telegram", "discord", ...
    chat_id: str     # Chat identifier
    chat_type: str   # "dm" or "group"
    user_id: str     # Who sent the message

Dù là WeChat hay Telegram thì sau khi dịch đều giống nhau MessageEvent. Mã xuôi dòng không cần biết tin nhắn đến từ nền tảng nào.

Logic dịch của bộ chuyển đổi WeChat:

class WeComAdapter:
    """WeCom adapter: connects to WeChat, receives messages, translates, sends replies."""

    async def connect(self):
        """Connect to the WeCom WebSocket."""
        self._ws = await session.ws_connect("wss://openws.work.weixin.qq.com")
        await self._ws.send_json({
            "cmd": "aibot_subscribe",
            "body": {"bot_id": self._bot_id, "secret": self._secret},
        })
        # Start background listener
        asyncio.create_task(self._listen_loop())

    async def _listen_loop(self):
        """Continuously read WeChat messages, translate to MessageEvent, pass to callback."""
        while True:
            raw = await self._ws.receive_json()
            if raw["cmd"] == "aibot_msg_callback":
                event = self._translate(raw["body"])
                await self._on_message(event)  # This callback is registered from outside

    def _translate(self, body: dict) -> MessageEvent:
        """WeChat format -> unified format. This is the adapter's core job."""
        return MessageEvent(
            message_id=body["msgid"],
            text=body["text"]["content"],
            source=SessionSource(
                platform="wecom",
                chat_id=body["chatid"],
                chat_type="dm" if body["chattype"] == "single" else "group",
                user_id=body["from"]["userid"],
            ),
            message_type="text",
        )

    async def send(self, chat_id: str, content: str):
        """Unified reply text -> WeChat format, send out."""
        await self._ws.send_json({
            "cmd": "aibot_send_msg",
            "body": {
                "chatid": chat_id,
                "msgtype": "markdown",
                "markdown": {"content": content[:4000]},
            },
        })

Nếu bạn muốn thêm Telegram, hãy viết một TelegramAdapter với logic dịch khác nhau nhưng tạo ra cùng một kết quả MessageEvent định dạng. Không cần thay đổi một dòng mã nào ở cuối dòng.

Cấu trúc bây giờ trông giống như:

WeComAdapter ----translate----> MessageEvent ----> find session -> call loop -> reply
TelegramAdapter --translate---> MessageEvent ---->  same as above
DiscordAdapter ---translate---> MessageEvent ---->  same as above

Phần giữa -- "tìm phiên -> vòng lặp cuộc gọi -> trả lời" -- giống hệt nhau cho tất cả các nền tảng. agent Hermes gói nó thành một chức năng duy nhất và đặt tên cho nó: _handle_message.

Khi mỗi bộ điều hợp khởi động, nó sẽ đăng ký chức năng này làm hàm gọi lại. Sau khi dịch một tin nhắn nhận được, nó sẽ gọi hàm mà không cần biết chuyện gì xảy ra bên trong.

# At startup
wecom_adapter._on_message = handle_message    # Register callback
telegram_adapter._on_message = handle_message  # Same function

# At runtime
# WeChat message arrives -> WeComAdapter._listen_loop -> _translate -> handle_message(event)
# Telegram message arrives -> TelegramAdapter._poll_loop -> _translate -> handle_message(event)

Vậy ai quản lý việc "khởi động tất cả các bộ điều hợp" và "cung cấp điều này handle_message"?

Đương nhiên đến GatewayRunner

Bây giờ bạn có nhiều bộ điều hợp và cần ai đó thực hiện ba việc:

  1. Khi khởi động: Lặp lại cấu hình, kết nối tất cả các bộ điều hợp đã bật
  2. Khi chạy: Sau khi nhận được MessageEvent, tìm phiên, gọi vòng lặp, trả lời
  3. Khi tắt máy: Ngắt kết nối tất cả các kết nối một cách khéo léo

Gói ba trách nhiệm này vào một lớp và bạn nhận được GatewayRunner:

class GatewayRunner:
    def __init__(self, config):
        self.adapters = {}       # platform -> adapter
        self.session_store = {}  # session_key -> message history

    async def start(self):
        """Start all configured platform adapters."""
        for platform, platform_config in config.platforms.items():
            if not platform_config.enabled:
                continue

            # Create adapter
            if platform == "wecom":
                adapter = WeComAdapter(platform_config)
            elif platform == "telegram":
                adapter = TelegramAdapter(platform_config)
            # ...

            # Register handle_message as callback
            adapter._on_message = self._handle_message

            # Connect
            await adapter.connect()
            self.adapters[platform] = adapter

    async def _handle_message(self, event: MessageEvent) -> str:
        """
        All platform messages ultimately arrive here.
        This function doesn't know whether the message came from WeChat or Telegram --
        it only sees the MessageEvent.
        """
        # 1. Generate session key
        src = event.source
        if src.chat_type == "dm":
            session_key = f"agent:main:{src.platform}:dm:{src.chat_id}"
        else:
            session_key = f"agent:main:{src.platform}:group:{src.chat_id}:{src.user_id}"

        # 2. Retrieve or create agent (cached by session key for reuse)
        if session_key not in self.agents:
            self.agents[session_key] = AIAgent(model="...", session_id=session_key)
        agent = self.agents[session_key]

        # 3. Reload the latest history from the database every time
        #    Why not use the agent's internal history? Because it may have been
        #    modified externally (e.g., user ran /undo to delete the last turn,
        #    or context compression was triggered)
        history = self.session_store.load_transcript(session_key)

        # 4. Call the core loop with the latest history
        response = agent.run_conversation(event.text, conversation_history=history)

        return response

    async def stop(self):
        for adapter in self.adapters.values():
            await adapter.disconnect()

Lưu ý hai quyết định thiết kế:

Các phiên bản agent được lưu vào bộ nhớ đệm theo khóa phiên để sử dụng lại, không được tạo mới cho mỗi tin nhắn. Zhang San gửi 10 tin nhắn, tất cả đều được xử lý bởi cùng một phiên bản agent. Nhưng Zhang San trên WeChat và Bob trên Telegram có các khóa phiên khác nhau nên họ nhận được các phiên bản agent khác nhau.

Lịch sử được tải lại từ cơ sở dữ liệu mỗi lần, không dựa vào bộ nhớ trong của agent. Điều này là do lịch sử có thể được sửa đổi từ bên ngoài -- người dùng đã chạy /undo (xóa lượt cuối cùng), /compress (ngữ cảnh nén) hoặc phiên đã hết hạn và được đặt lại tự động. Nếu agent chỉ sử dụng lịch sử được lưu trong bộ nhớ đệm nội bộ thì những sửa đổi này sẽ bị mất.

GatewayRunner không phải là một khái niệm trừu tượng bạn cần ghi nhớ. Đó là nội dung mà bạn sẽ tự viết -- khi bạn có nhiều bộ chuyển đổi cần quản lý thống nhất, bạn sẽ hợp nhất logic "bắt đầu", "tuyến đường" và "tắt" vào một nơi một cách tự nhiên.

Bây giờ hãy xem cấu trúc đầy đủ:

GatewayRunner
  |
  +-- At startup: create adapters, register callbacks, connect to platforms
  |
  +-- Agent cache pool (session key -> agent instance)
  |
  +-- WeComAdapter (WeChat)
  |    +-- Receives WeChat message -> _translate -> calls GatewayRunner._handle_message
  |
  +-- TelegramAdapter (Telegram)
  |    +-- Receives Telegram message -> _translate -> calls GatewayRunner._handle_message
  |
  +-- _handle_message (convergence point for all messages)
       +-- Find session -> retrieve/create agent -> load history from database
         -> agent.run_conversation(message, history) -> return reply

agent.run_conversation() không biết tin nhắn đến từ đâu. Dù là WeChat hay Telegram, cuộc gọi đều giống hệt nhau. Đây là cái gì s00 có nghĩa là "sự khác biệt duy nhất giữa kịch bản Gateway và CLI là điểm vào và điểm thoát; vòng lặp cốt lõi giống hệt nhau."

Vấn đề 3: Nếu Zhang San gửi một tin nhắn khác trong khi người đại diện đang suy nghĩ thì sao?

Zhang San gửi cho tôi "Viết cho tôi một thuật toán sắp xếp" và người đại diện bắt đầu suy nghĩ. 10 giây sau, Trương Tam phát ra "Sử dụng Python."

Nếu bạn tạo một phiên bản agent khác cho tin nhắn thứ hai, hai phiên bản chạy đồng thời và đọc/ghi lịch sử phiên của Zhang San sẽ tạo ra các câu trả lời bị cắt xén.

Nhưng bạn cũng không thể loại bỏ tin nhắn thứ hai -- người dùng thực sự muốn làm rõ thêm.

Cách tiếp cận của agent Hermes: Mỗi lần chỉ có một agent chạy mỗi phiên. Tin nhắn mới được lưu vào bộ nhớ đệm và xử lý sau khi agent hiện tại kết thúc.

# Core logic in the adapter base class (simplified)

active_sessions = {}   # session_key -> interrupt signal
pending_messages = {}  # session_key -> buffered next message

async def handle_message(self, event):
    session_key = build_session_key(event.source)

    if session_key in active_sessions:
        # An agent is already running for this session
        # Buffer the new message (only keep the last one; earlier ones get overwritten)
        pending_messages[session_key] = event
        # Send an interrupt signal to the running agent
        active_sessions[session_key].set()
        return

    # No active agent -> mark as active, start processing
    active_sessions[session_key] = asyncio.Event()
    await self._process_message_background(event, session_key)

Sau khi hoàn thành một tin nhắn, hãy kiểm tra các tin nhắn được lưu vào bộ đệm:

# At the end of _process_message_background
if session_key in pending_messages:
    next_event = pending_messages.pop(session_key)
    del active_sessions[session_key]
    # Process the next message immediately (not re-queuing, but calling directly)
    await self._process_message_background(next_event, session_key)
else:
    del active_sessions[session_key]

Tín hiệu ngắt thực sự làm gì?

"Gửi tín hiệu ngắt" không phải là một tuyên bố mơ hồ. agent.interrupt() thực hiện một điều rất cụ thể: đặt _interrupt_requested = True cờ.

Vòng lặp cốt lõi của agent (vòng lặp while từ s01) kiểm tra cờ này ở nhiều điểm:

# 1. Waiting for LLM response stream -> stop reading immediately
with client.responses.stream(**api_kwargs) as stream:
    for event in stream:
        if self._interrupt_requested:
            break  # Stop waiting for remaining tokens from the LLM

# 2. Executing multiple tool calls sequentially -> skip the rest
for i, tool_call in enumerate(tool_calls):
    execute(tool_call)
    if self._interrupt_requested and i < len(tool_calls):
        # Skip unexecuted tools, fill in a "skipped" placeholder result
        for skipped in tool_calls[i:]:
            messages.append({
                "role": "tool",
                "content": "[Tool execution skipped -- user sent a new message]",
                "tool_call_id": skipped.id,
            })
        break

# 3. Also checked at the start of each loop iteration
while iteration < max_iterations:
    if self._interrupt_requested:
        break  # Exit loop, return whatever results exist so far

Vì vậy, sự gián đoạn không phải là "bắt buộc hủy bỏ một quá trình" mà là thoát ra một cách duyên dáng ở điểm kiểm tra tiếp theo -- dừng chờ trên luồng, bỏ qua các công cụ còn lại, thoát khỏi vòng lặp.

Nội dung bị gián đoạn có bị mất không? Cuộc trò chuyện có mạch lạc không?

Không có gì bị mất và sự mạch lạc vẫn được duy trì.

Sau khi bị gián đoạn, agent quay trở lại result["messages"] như bình thường, chứa mọi thứ được tạo ra cho đến điểm gián đoạn: câu trả lời được tạo một phần, lệnh gọi công cụ được thực hiện có kết quả và lệnh gọi công cụ bị bỏ qua với thông báo công cụ giữ chỗ (trình giữ chỗ đáp ứng yêu cầu cứng của API OpenAI rằng "mọi lệnh gọi công cụ phải có phản hồi công cụ"). Cổng vào append_to_transcriptTất cả điều này được đưa vào cơ sở dữ liệu.

Ở vòng tiếp theo xử lý tin nhắn mới, conversation_history được tải lại từ cơ sở dữ liệu. agent mới nhìn thấy toàn bộ chủ đề:

user:      "Write me a sorting algorithm"
assistant: "Sure, I'd recommend quicksort. First..."            <-- partial content generated before interruption
tool_call: search_algorithm("sorting")                          <-- executed
tool:      "[search results...]"
tool_call: write_file("sort.py", ...)                           <-- interrupted, skipped
tool:      "[Tool execution skipped -- user sent a new message]"
user:      "Use Python"                                          <-- new message

agent có thể nhìn thấy dấu vết bị gián đoạn - nó biết lượt trước đó đã dừng ở đâu và những gì vẫn cần làm - vì vậy, nó có thể tiếp tục mạch lạc thay vì hành động như thể "quên mất mình đang làm gì".

Đi qua một kịch bản cụ thể

Zhang San sends "Write me a sorting algorithm"
  -> No entry for Zhang San in active_sessions -> mark active, start background task
  -> Agent starts calling the LLM...

Zhang San sends "Use Python" (agent is waiting for LLM streaming response)
  -> Zhang San found in active_sessions -> buffer this message
  -> Call agent.interrupt("Use Python")
  -> agent._interrupt_requested = True
  -> Agent checks flag at next stream event -> break out of stream
  -> Agent finishes current loop, returns partial reply
  -> Partial reply sent to Zhang San
  -> Check pending_messages -> found "Use Python"
  -> Load latest history from database (includes message 1 and partial reply) -> process message 2

Li Si sends "What's the weather today" (at the same time as Zhang San)
  -> Different session key -> completely independent background task, runs in parallel with Zhang San

Lưu ý một chi tiết: pending_messages chỉ giữ tin nhắn cuối cùng (gán trực tiếp, không thêm vào danh sách). Nếu Zhang San gửi ba thông báo liên tiếp nhanh chóng -- "Sử dụng Python", "sắp xếp nhanh", "thêm nhận xét" -- thì chỉ "thêm nhận xét" mới được xử lý. Điều này là có chủ ý -- trên các nền tảng nhắn tin, nhiều tin nhắn được người dùng gửi liên tiếp nhanh chóng thường có cùng một ý tưởng. Chỉ xử lý cái cuối cùng là đủ.

Đi qua toàn bộ quy trình với WeChat

Bây giờ hãy xâu chuỗi tất cả các khái niệm lại với nhau. Bạn gửi tin nhắn riêng tới bot agent Hermes trong WeCom: "Kiểm tra thời tiết hôm nay cho tôi."

1. Máy chủ WeChat đẩy tin nhắn đến bộ chuyển đổi

WeCom đẩy JSON qua WebSocket:

{
    "cmd": "aibot_msg_callback",
    "body": {
        "msgid": "msg_001",
        "msgtype": "text",
        "from": {"userid": "zhangsan"},
        "chatid": "zhangsan",
        "chattype": "single",
        "text": {"content": "Check today's weather for me"}
    }
}

2. Adaptor dịch sang MessageEvent

WeComAdapter._translate() chuyển đổi JSON của WeChat thành định dạng hợp nhất:

MessageEvent(
    message_id="msg_001",
    text="Check today's weather for me",
    source=SessionSource(platform="wecom", chat_id="zhangsan",
                         chat_type="dm", user_id="zhangsan"),
    message_type="text",
)

Các trường dành riêng cho WeChat như cmd, headersvà req_id đã biến mất -- MessageEvent chỉ giữ lại những thông tin chung trên tất cả các nền tảng.

3. Kiểm tra hàng đợi

Bộ điều hợp kiểm tra active_sessions: hiện không có tin nhắn nào đang được xử lý zhangsan. Đánh dấu là đang hoạt động, tiếp tục.

4. GatewayRunner tiếp quản

_handle_message(event) được gọi là:

# Generate session key
session_key = "agent:main:wecom:dm:zhangsan"

# Retrieve cached agent (create new one if this is Zhang San's first message)
agent = agents.get(session_key) or AIAgent(model=model, ...)

# Load latest history from database (not relying on agent's internal memory)
history = session_store.load_transcript(session_key)

# Call core loop with history (same method the CLI calls)
response = agent.run_conversation(user_message, conversation_history=history)
# -> "Today in Beijing it's sunny, high of 28C..."

5. Trả lời được gửi lại WeChat

GatewayRunner trả về văn bản trả lời và WeComAdapter.send() gói nó ở định dạng WeChat:

{
    "cmd": "aibot_send_msg",
    "body": {
        "chatid": "zhangsan",
        "msgtype": "markdown",
        "markdown": {"content": "Today in Beijing it's sunny, high of 28C..."}
    }
}

Người dùng nhìn thấy câu trả lời của tổng đài viên trong ứng dụng khách WeCom.

Nếu bạn cũng đã kết nối Telegram

Một người dùng Telegram gửi tin nhắn. TelegramAdapter nhận nó, dịch nó sang một MessageEvent (platform="telegram") và gọi tương tự _handle_message. Sự khác biệt duy nhất là khóa phiên trở thành agent:main:telegram:dm:12345.

Không một dòng mã vòng lặp lõi nào bị thay đổi.

Lựa chọn thiết kế độc đáo của agent Hermes

Việc thực hiện ở trên là một phiên bản giảng dạy đơn giản hóa. agent Hermes thực sự cũng xử lý một số vấn đề trong thế giới thực không thể bỏ qua:

1. Phân mảnh tin nhắn WeChat

Ứng dụng khách WeCom tự động cắt ngắn các tin nhắn dài 4000 ký tự. Nếu người dùng gửi 6000 ký tự văn bản, WeChat sẽ chia nó thành hai tin nhắn.

Nếu agent trả lời riêng từng "nửa tin nhắn" thì các câu trả lời sẽ không mạch lạc.

Bộ chuyển đổi giải quyết vấn đề này bằng cửa sổ thời gian: sau khi nhận được đoạn đầu tiên, bộ chuyển đổi sẽ đợi 0,6 giây. Nếu đoạn thứ hai đến và đoạn đầu tiên có gần 4000 ký tự, nó sẽ tiếp tục chờ tối đa 2 giây. Sau thời gian chờ, tất cả các đoạn được ghép nối và được coi là một thông báo hoàn chỉnh duy nhất.

2. Tự động hết hạn phiên

Trong kịch bản CLI, việc đóng terminal sẽ kết thúc cuộc trò chuyện. Nhưng Gateway là một dịch vụ hoạt động lâu dài -- bối cảnh của người dùng từ ba tháng trước vẫn còn đó.

Hermes Agent hỗ trợ hai chế độ tự động thiết lập lại:

  • Thời gian chờ không hoạt động: Không có tin nhắn mới trong hơn 24 giờ -> tự động bắt đầu phiên mới ở tin nhắn tiếp theo
  • Đặt lại hàng ngày: Tất cả các phiên bị xóa vào lúc 4 giờ sáng hàng ngày (bộ nhớ không bị mất; chỉ khởi động lại lịch sử hội thoại)

3. Phục hồi sự cố

Nếu Cổng gặp sự cố và khởi động lại, các yêu cầu đang thực hiện có thể ở trạng thái hoàn thành một nửa.

Khi khởi động, nó không cố gắng khôi phục các trạng thái chưa hoàn thành một nửa này. Thay vào đó, các phiên hoạt động trong vòng 120 giây qua sẽ được đánh dấu là "bị treo" và tự động đặt lại ở thư đến tiếp theo. Bắt đầu sạch sẽ an toàn hơn nhiều so với việc khôi phục trạng thái bẩn.

4. Hai chế độ tích hợp WeChat

WeCom cung cấp hai phương thức tích hợp bot và agent Hermes có bộ chuyển đổi cho mỗi phương thức:

Bot WebSocket (WeComAdapter)Gọi lại HTTP (WecomCallbackAdapter)
Modal kết nốiWebSocket liên tục, hai chiều thời gian thựcMáy chủ HTTP, nhận các lệnh gọi lại XML được mã hóa
Truyền thôngTất cả hình ảnh, video, giọng nói, tập tin đều được hỗ trợChỉ văn bản
Đa ứng dụngBot đơnCó thể kết nối nhiều ứng dụng tùy chỉnh
Trường hợp sử dụngTích hợp AI Bot nhanh chóngỨng dụng tùy chỉnh dành cho doanh nghiệp cần kiểm soát chi tiết

Hai chế độ hoạt động ở bên ngoài khác nhau, nhưng cả hai đều xuất ra cùng một kết quả MessageEvent. Chúng hoàn toàn minh bạch với GatewayRunner.

Cách nó kết nối với vòng lặp chính

Giống như tất cả các chương trước, Gateway được lắp ráp bên ngoài vòng lặp lõi. Vòng lặp lõi không biết Cổng tồn tại.

CLI launch:
  User input -> create AIAgent -> agent.run_conversation(user input)

Gateway launch:
  1. load_config()         -> read which platforms to enable, their tokens
  2. GatewayRunner(config) -> create runner
  3. runner.start()        -> start all adapters, register callbacks
  4. Wait for messages     -> adapter translates -> _handle_message
                              -> create AIAgent -> agent.run_conversation(message text)

Cả hai đường dẫn cuối cùng đều gọi agent.run_conversation(). AIAgent chỉ nhận thông số: model, lịch sử tin nhắn. Nó không quan tâm người gọi là ai.

Những sai lầm phổ biến nhất của người mới bắt đầu

1. Đưa những khác biệt về nền tảng vào trong vòng lặp cốt lõi

"Mức giảm giá của WeChat khác với mức giảm của Telegram -- chỉ cần thêm if vào vòng lặp?" -- Đừng. Chuyển đổi định dạng là của bộ chuyển đổi send() trách nhiệm. Vòng lặp lõi chỉ xuất ra văn bản chung.

2. Tạo agent mới cho mỗi tin nhắn

Người dùng gửi ba tin nhắn liên tiếp. Nếu mỗi cái tạo ra một cá thể agent riêng, thì tất cả chúng đều đọc và ghi cùng một lịch sử, tạo ra các câu trả lời xung đột nhau. Các tin nhắn cho cùng một khóa phiên phải được tuần tự hóa.

3. Kích thước khóa phiên không đủ

Nếu chìa khóa chỉ platform:chat_id, mọi người trong cùng một nhóm sẽ chia sẻ một cuộc trò chuyện. Thông thường bạn cần thêm user_id để cách ly từng người dùng trong nhóm -- trừ khi bạn cố tình muốn các thành viên trong nhóm chia sẻ bối cảnh.

4. Bỏ qua việc trùng lặp tin nhắn

Trong thời gian mạng không ổn định, cùng một tin nhắn có thể được đẩy hai lần. Tất cả các bộ điều hợp cần được loại bỏ trùng lặp bằng cách message_id.

Phạm vi của chương này

Chương này trình bày kỹ lưỡng ba điều:

  1. Tại sao cần có bộ điều hợp -- Bắt nguồn từ "cách triển khai đơn giản nhất có thể", không được xác định trong chân không
  2. Những gì GatewayRunner làm -- Khởi động bộ điều hợp, định tuyến thông báo, gọi vòng lặp. Thế thôi
  3. Cách tách phiên -- Quy tắc tạo khóa phiên, tại sao những người dùng khác nhau trong cùng một nhóm lại có các khóa khác nhau

Các chủ đề bị trì hoãn:

  • Triển khai cụ thể từng bộ điều hợp nền tảng -> mô hình giống nhau; hiểu WeChat là đủ
  • Cách các tác vụ được lên lịch mang lại kết quả cho các nền tảng khác nhau -> s15
  • Trừu tượng hóa backend terminal (Docker / SSH) -> s14
  • Móc cấp cổng -> tương tự như s08

Nếu người đọc có thể đi từ "vòng lặp tạm thời chỉ kết nối với WeChat" đến hiểu "tại sao chúng ta cần bộ điều hợp, tại sao chúng ta cần định dạng tin nhắn thống nhất và tại sao chúng ta cần bộ định tuyến tập trung", thì chương này đã phục vụ được mục đích của nó.

Sau chương này, bạn sẽ có thể trả lời

  • Nếu bạn chỉ kết nối với một nền tảng, bạn có cần GatewayRunner không? Tại thời điểm nào bạn bắt đầu cần nó?
  • Có vấn đề gì MessageEvent giải quyết? Không có nó, mã nào cần thay đổi khi thêm nền tảng mới?
  • Zhang San và Li Si trong cùng một nhóm WeChat có cùng khóa phiên không? Tại sao hoặc tại sao không?
  • Chuyện gì sẽ xảy ra khi Zhang San gửi một tin nhắn khác trong khi người đại diện đang suy nghĩ?
  • Làm agent.run_conversation() biết tin nhắn đến từ WeChat hay CLI?

Trong một câu: Cổng bắt đầu từ một câu hỏi -- "làm cách nào để tôi có được cùng một đại diện làm việc trên WeChat?" Sau đó, bạn phát hiện ra rằng mình cần cách ly phiên (khóa phiên), hợp nhất định dạng (MessageEvent) và một vị trí trung tâm để quản lý nhiều nền tảng (GatewayRunner). Đây không phải là những thứ mà một kiến ​​trúc sư mơ ước trong chân không -- chúng phát triển một cách tự nhiên từ những vấn đề cụ thể.

Mục lục nguồn