+2

LLM Agent: Hướng dẫn cơ bản về LLM Agent

TỔNG QUAN

LLM Agent liên quan đến các ứng dụng LLM có thể thực thi các tác vụ phức tạp thông qua việc sử dụng kiến trúc kết hợp với LLM và các module chính như: planing, memory và tools . Trong hệ thống của Agent, LLM đóng vai trò là “bộ não điều khiển chính” điều khiển luồng hoạt động cần thiết để hoàn thành một nhiệm vụ hoặc yêu cầu từ người dùng.

Ví dụ

Giả sử chúng ta xây dựng một hệ thống có thể giúp trả lời câu hỏi sau: “ Lượng khí CO2 phát thải trung bình năm 2024 ở Việt Nam là bao nhiêu?”

Câu hỏi trên có thể được trả lời bằng những mô hình LLM đã có kiến thức cần thiết để trả lời câu hỏi đó. Trong trường hợp các mô hình LLM chưa có kiến thức cụ thể về câu hỏi thì có thể sử dụng hệ thống RAG đơn giản trong đó LLM có quyền truy cập vào các báo cáo hoặc các thông tin có liên quan đến thống kê lượng khí thải tại Việt Nam năm 2024.

Bây giờ hãy yêu cầu cho hệ thống một câu hỏi phức tạp hơn như sau: “ Xu hướng lượng khí CO2 phát thải trung bình hằng năm Ở Việt Nam đã thay đổi như thế nào trong thập kỷ qua và điều này có tác động như thế nào đến các chỉ số về chất lượng không khí? Ngoài ra bạn có thể cung cấp biểu đồ thể hiện xu hướng về chất lượng không khí trong giai đoạn này hay không?”.

Để giải quyết những câu hỏi phức tạp như trên chỉ sử dụng các mô hình LLM thôi là không đủ. Bởi vì nó yêu cầu LLM chia nhiệm vụ thành các phần nhỏ có thể xử lý được thông qua việc sử dụng các tools và một luồng hoạt động để có phản hồi cuối cùng mong muốn. Một giải pháp khả thi là xây dựng một LLM Agent có quyền truy cập vào API tìm kiếm, các ấn phẩm liên quan đến vấn đề phát thải và môi trường. Điều đó đảm bảo đưa đến câu trả lời đáng tin cậy cho người dùng. Ngoài ra, LLM sẽ cần sử dụng công cụ “code interpreter” giúp lấy các dữ liệu liên quan để tạo ra các biểu đồ hữu ích mà người dùng yêu cầu. Đây là những công cụ bậc cao có thể có trong một LLM Agent nhưng vẫn còn những yếu tố cần quan trọng cần cân nhắc, chẳng hạn như việc lên plan để giải quyết nhiệm vụ và truy cập vào bộ nhớ giúp Agent theo dõi trạng thái và các luồng hoạt động , đánh giá được tiến độ tổng thể của nhiệm vụ.

AGENT

Hệ thống Agent được điều khiển bởi LLMs được mô tả trong hình sau:

Trong các hệ thống Agent , LLM đóng vai trò như bộ não của Agent. Đồng thời, hệ thống này bao gồm 3 phần chính:

  • Planning: Agent sẽ phân chia nhiệm vụ lớn thành các nhiệm vụ con và lập kế hoạch quy trình thực hiện nhiệm vụ. Trong quá trình thực hiện, Agent sẽ suy nghĩ và phản ánh về tiến độ, từ đó quyết định tiếp tục thực hiện hay đánh giá rằng nhiệm vụ đã hoàn thành và kết thúc hoạt động.
  • Memory:
    • Short-term memory: Là ngữ cảnh trong quá trình thực hiện nmhiệm vụ, được tạo ra và lưu trữ tạm thời khi thực hiện các nhiệm vụ con. Dữ liệu này sẽ bị xóa khi nhiệm vụ hoàn tất.
    • Long-term memory: là thông tin được lưu trữ lâu dài, thường liên quan đến cơ sở tri thức bên ngoài. Thông thường, các cơ sở tri thức này được lưu trữ và truy xuất thông qua cơ sở dữ liệu dạng vector.
  • Tools: Agent được tích hợp các API công cụ như: máy tính, công cụ tìm kiếm, trình biên dịch mã, công cụ truy vấn cơ sở dữ liệu,..v.v. Với các API này, Agent có thể tương tác trực tiếp và giải quyết các vấn đề thực tế.

1. PLANING

Planing có thể được hiểu là quá trình quan sát và suy nghĩ. Nếu so sánh với con người, khi nhận một nhiệm vụ, cách chúng ta suy nghĩ có thể diễn ra như sau:

  1. Chúng ta đầu tiên suy nghĩ về cách hoàn thành nhiệm vụ này.
  2. Sau đó xem xét các công cụ sẵn có và cách sử dụng chúng hiệu quả để đạt được mục tiêu.
  3. Tiếp theo, chúng ta chia nhỏ nhiệm vụ thành các nhiệm vụ con.
  4. Trong quá trình thực hiện nhiệm vụ, chúng ta thường xuyên suy ngẫm và điều chỉnh để cải thiện hiệu quả, đồng thời rút kinh nghiệm cho các bước tiếp theo.
  5. Cuối cùng, trong quá trình thực hiện, chúng ta luôn đánh giá xem nhiệm vụ có thể kết thúc khi nào.

Đây chính là khả năng lập kế hoạch của con người. Mục tiêu là xây dựng Agent có thể sở hữu tư duy tương tự thông qua việc sử dụng kỹ thuật Prompt Engineering của LLM, giúp tạo ra một mô hình suy nghĩ giống con người. Trong Agent, điều quan trọng nhất là phải làm cho LLM có được hai khả năng sau:

  1. Khả năng tư duy có cấu trúc: Có khả năng phân tích nhiệm vụ, lập kế hoạch logic và xử lý thông tin theo cách có hệ thống.
  2. Khả năng phản ánh và học tập liên tục: Biết cách tự đánh giá hiệu quả, cải thiện trong quá trình thực hiện, và rút kinh nghiệm từ những sai lầm.

1.1 Phân chia nhiệm vụ con

Thông qua LLM, Agent có thể phân chia nhiệm vụ lớn thành các nhiệm vụ con nhỏ hơn và dễ kiểm soát hơn, từ đó giúp thực hiện các nhiệm vụ phức tạp một cách hiệu quả.

1.1.1 Chuỗi suy nghĩ (Chain of Thoughts, CoT)

Chuỗi suy nghĩ đã trở thành một kỹ thuật nhắc (prompt technique) tiêu chuẩn, giúp nâng cao đáng kể hiệu quả của LLM khi xử lý các nhiệm vụ phức tạp. Khi chúng ta hướng dẫn LLM với yêu cầu như "think step by step" (nghĩ từng bước một), LLM sẽ phân chia vấn đề thành nhiều bước, suy nghĩ và giải quyết từng bước. Điều này giúp kết quả đầu ra chính xác hơn nhờ cách suy nghĩ tuyến tính. Prompt sử dụng chuỗi suy nghĩ có thể như sau (đây chỉ là một ví dụ rất cơ bản, trong thực tế sẽ cần tối ưu hóa thêm theo yêu cầu):

Prompt mẫu cơ bản:

Bước 1: Hiểu câu hỏi.

Bước 2: Chia nhỏ vấn đề thành các bước logic.

Bước 3: Thực hiện từng bước với các suy nghĩ chi tiết.

Ví dụ yêu cầu: "Hãy giải quyết vấn đề này từng bước để đảm bảo tính chính xác trong từng phần."

Prompt như vậy sẽ khuyến khích LLM phân tích vấn đề một cách tuần tự và sâu sắc hơn.

1.1.2 Cây suy nghĩ (Tree-of-Thought, ToT)

Cây suy nghĩ là một sự mở rộng hơn nữa của chuỗi suy nghĩ (CoT). Trong mỗi bước của CoT, thay vì chỉ suy diễn một hướng, LLM có thể đưa ra nhiều nhánh suy luận, từ đó tạo thành một cây suy nghĩ với cấu trúc phân nhánh. Quá trình này bao gồm:

  • Phân nhánh suy luận: Ở mỗi bước, LLM sẽ suy diễn nhiều khả năng hoặc kịch bản, mở rộng thành một cấu trúc giống như cây.
  • Đánh giá các nhánh: Sử dụng phương pháp heuristic (phương pháp dựa trên kinh nghiệm hoặc quy tắc gần đúng) để đánh giá mức độ đóng góp của từng nhánh suy luận vào việc giải quyết vấn đề.
  • Chọn thuật toán tìm kiếm: Sử dụng các thuật toán như tìm kiếm theo chiều rộng (Breadth-First Search, BFS) hoặc tìm kiếm theo chiều sâu (Depth-First Search, DFS) để khám phá và tìm kiếm trong cây suy nghĩ.
  • Dự đoán và quay lui: Tận dụng cơ chế dự đoán để mở rộng nhánh mới hoặc quay lại các nhánh trước nếu cần.

Kỹ thuật ToT cho phép mô hình thực hiện suy nghĩ đa chiều và linh hoạt hơn, giúp giải quyết các vấn đề phức tạp bằng cách xem xét đồng thời nhiều khả năng và phương án tối ưu.

1.2 Đánh giá và cải thiện

Trong quá trình thực hiện nhiệm vụ, Agent sử dụng LLM để đánh giá về các nhiệm vụ phụ đã hoàn thành, rút kinh nghiệm từ những sai lầm và cải thiện nó trong tương lai để cải thiện chất lượng hoàn thành nhiệm vụ. Đồng thời, suy xét xem nhiệm vụ đã hoàn thành chưa và chấm dứt nhiệm vụ đó.

ReAct

Nghiên cứu này(https://arxiv.org/abs/2210.03629?login=from_csdn) đề xuất một phương pháp tăng cường các mô hình ngôn ngữ lớn, trong đó tăng cường khả năng suy luận bằng cách kết hợp lý luận (Reasoning) và hành động (Acting) để tăng cường khả năng lý luận và ra quyết định.

  • Reasoning: Là quá trình rút ra kết luận dự trên kiến thức hiện có hoặc kiến thức thu được sau khi hành động
  • Acting: LLM sử dụng các công cụ để thu nhập kiến thức dựa trên tình hình thực tế hoặc hoàn thành các nhiệm vụ phụ để thu nhập thông tin theo giai đoạn

Tại sao việc kết hợp lý luận và hành động lại nâng cao hiệu quả khả năng hoàn thành nhiệm vụ của LLM? Câu hỏi này thực ra rất dễ trả lời. Hãy lấy yêu cầu sau làm ví dụ :” Nghiên cứu về 2 hệ thống lái xe tự động của Tesla FSD và Huawei ADS”.

  • Chỉ suy luận (Reasoning Only): LLM chỉ thực hiện suy luận dựa trên kiến thức sẵn có để tạo ra câu trả lời cho câu hỏi. Rõ ràng, nếu LLM không sở hữu kiến thức liên quan, nó có thể mắc sai lầm, dẫn đến việc tạo ra thông tin sai lệch hoặc trả lời một cách tùy tiện.
  • Chỉ hành động (Acting Only): Mô hình không thực hiện suy luận mà chỉ sử dụng công cụ (ví dụ: công cụ tìm kiếm) để tra cứu câu hỏi. Kết quả thu được sẽ là một lượng lớn thông tin nhưng không thể trực tiếp trả lời câu hỏi ban đầu.
  • Suy luận + Hành động (Reasoning and Acting): LLM đầu tiên sẽ suy luận dựa trên kiến thức sẵn có và kiểm tra những công cụ có sẵn. Khi phát hiện rằng kiến thức hiện tại không đủ để trả lời câu hỏi, nó sẽ sử dụng Tools, chẳng hạn như tool tìm kiếm hoặc tool tạo báo cáo, để thu thập thông tin mới. Sau đó, LLM sẽ tiếp tục suy luận dựa trên thông tin mới, lặp lại quá trình suy luận và hành động cho đến khi hoàn thành nhiệm vụ.

Các bước suy luận và hành động sẽ diễn ra như sau:

Suy luận 1: Kiến thức hiện tại không đủ để trả lời câu hỏi này. Để trả lời câu hỏi, cần phải biết "Tesla FSD" và "Huawei ADS" là gì.

Hành động 1: Sử dụng công cụ tìm kiếm để tìm kiếm thông tin về "Tesla FSD" và "Huawei ADS".

Quan sát 1: Tóm tắt nội dung từ hành động 1.

Suy luận 2: Dựa trên thông tin từ hành động 1 và quan sát 1, nhận thấy đây là một bài toán so sánh các giải pháp của hai nhà cung cấp công nghệ lái xe tự động. Dựa trên thông tin hiện có, bây giờ cần phải tạo một báo cáo.

Hành động 2: Sử dụng công cụ tạo báo cáo để tạo báo cáo nghiên cứu.

Quan sát 2: Nhiệm vụ đã hoàn thành.

Thông qua việc thiết kế các prompt thông minh, LLM có thể lặp lại quá trình suy luận và hành động một cách có hệ thống, cuối cùng hoàn thành nhiệm vụ. Mẫu prompt của phương pháp ReAct thường có cấu trúc cơ bản như sau:

  1. Suy luận (Reasoning): Đưa ra suy luận ban đầu dựa trên kiến thức hiện có và xác định các vấn đề hoặc câu hỏi cần giải quyết để tiếp tục nhiệm vụ.
    Ví dụ: "Dựa trên những gì tôi đã biết, tôi cần tìm hiểu thêm về [chủ đề] để có thể tiếp tục."

  2. Hành động (Acting): Thực hiện hành động dựa trên các suy luận đã được đưa ra, chẳng hạn như tìm kiếm thông tin, sử dụng công cụ hoặc truy vấn cơ sở dữ liệu.
    Ví dụ: "Sử dụng công cụ tìm kiếm để tìm thông tin về [chủ đề]."

  3. Quan sát (Observation): Tóm tắt kết quả thu được từ hành động đã thực hiện, cung cấp thông tin để tiếp tục suy luận.
    Ví dụ: "Kết quả từ tìm kiếm cho thấy rằng [tóm tắt thông tin tìm được]."

  4. Tiếp tục suy luận và hành động: Dựa trên quan sát và kết quả từ các bước trước, tiếp tục lặp lại quy trình suy luận và hành động để hướng đến mục tiêu cuối cùng, cho đến khi nhiệm vụ được hoàn thành.
    Ví dụ: "Dựa trên những thông tin mới có được, tôi cần [thực hiện hành động tiếp theo]."

Cấu trúc ReAct cho phép LLM liên tục thực hiện quá trình lập kế hoạch, hành động và phản hồi lại để điều chỉnh các bước tiếp theo, từ đó hoàn thành nhiệm vụ phức tạp.

2. MEMORY

Nhớ là gì? Khi chúng ta suy nghĩ về câu hỏi này, thực tế là bộ não con người đã sử dụng trí nhớ. Trí nhớ là khả năng lưu trữ, giữ lại và hồi tưởng thông tin của bộ não. Trí nhớ có thể được phân thành các loại khác nhau:

  • Trí nhớ ngắn hạn (hoặc trí nhớ làm việc): Đây là loại trí nhớ có thời gian duy trì ngắn, có thể lưu trữ và xử lý một lượng thông tin có hạn trong thời gian ngắn. Ví dụ, nhớ một số điện thoại cho đến khi bạn gọi xong.
  • Trí nhớ dài hạn: Đây là loại trí nhớ có thời gian lưu trữ dài, có thể lưu trữ một lượng lớn thông tin từ vài phút đến cả đời. Trí nhớ dài hạn có thể được chia thành trí nhớ rõ ràng và trí nhớ tiềm ẩn.
    • Trí nhớ rõ ràng: Là loại trí nhớ mà chúng ta có thể hồi tưởng và diễn đạt một cách có ý thức. Trí nhớ rõ ràng lại có thể phân thành trí nhớ tình huống (sự kiện cụ thể trong trải nghiệm cá nhân) và trí nhớ ngữ nghĩa (kiến thức và khái niệm chung).
    • Trí nhớ tiềm ẩn: Đây là loại trí nhớ thường không có ý thức, liên quan đến các kỹ năng và thói quen, như đạp xe hoặc gõ máy tính.

Mô phỏng theo cơ chế trí nhớ của con người, Agent đã thực hiện hai cơ chế trí nhớ sau:

  • Trí nhớ ngắn hạn(Short-term memory): Là thông tin được tạo ra trong quá trình thực hiện nhiệm vụ hiện tại, ví dụ như kết quả của một công cụ hoặc một nhiệm vụ con được thực hiện, sẽ được ghi vào trí nhớ ngắn hạn. Trí nhớ này được tạo ra và tạm lưu trong quá trình thực hiện nhiệm vụ, và sẽ bị xóa khi nhiệm vụ kết thúc.
  • Trí nhớ dài hạn(Long-term memory): Là thông tin được giữ lại trong thời gian dài. Thông thường, trí nhớ dài hạn ám chỉ các kho kiến thức ngoài, thường được lưu trữ và truy xuất qua cơ sở dữ liệu vector.

3. TOOLs

LLM là chương trình trong thế giới số. Để tương tác với thế giới thực, thu thập kiến thức mới hoặc tính toán các công thức phức tạp, LLM cần sử dụng công cụ. Vì vậy, chúng ta cần trang bị cho Agent các công cụ khác nhau và cung cấp khả năng sử dụng chúng.

Công cụ là gì? Nó có thể là búa, tua vít, hoặc cũng có thể là hàm (function), bộ công cụ phát triển phần mềm (SDK). Công cụ là hình thức cụ thể hóa trí tuệ của con người, giúp mở rộng khả năng và nâng cao hiệu quả công việc. Trong Agent, công cụ chính là các hàm (Function), và việc sử dụng công cụ chính là gọi hàm (Call Function).

Để thực hiện gọi hàm trong LLM, chúng ta sử dụng khả năng sau đây của LLM:

Function Calling

Function Calling là cơ chế giúp mô hình ngôn ngữ lớn kết nối với các công cụ bên ngoài. Khi gọi API LLM, bên gọi có thể mô tả các hàm, bao gồm mô tả chức năng của hàm, các tham số yêu cầu và các tham số phản hồi, để LLM có thể chọn hàm phù hợp dựa trên đầu vào của người dùng, đồng thời hiểu ngôn ngữ tự nhiên của người dùng và chuyển nó thành các tham số yêu cầu gọi hàm (được trả về dưới định dạng JSON). Bên gọi sẽ sử dụng tên hàm và tham số do LLM trả về để gọi hàm và nhận phản hồi. Cuối cùng, nếu cần, phản hồi từ hàm sẽ được chuyển cho LLM để LLM tổ chức lại thành câu trả lời bằng ngôn ngữ tự nhiên cho người dùng. Quy trình làm việc cụ thể của gọi hàm (function calling) như sau:

Các giao thức API của các LLM khác nhau sẽ có sự khác biệt, trong phần dưới đây, chúng ta sẽ lấy giao thức API của OpenAI làm ví dụ để giải thích cách thực hiện Function Calling.

Mô tả: Chúng ta có thể triển khai các hàm theo yêu cầu của Agent. Ví dụ như đối với Agent về " Nghiên cứu về 2 hệ thống lái xe tự động của Tesla FSD và Huawei ADS " trước đó, chúng ta có thể triển khai các hàm như: WebBrowseAndSummarize (duyệt web và tóm tắt nội dung trang web) và ConductResearch (tạo báo cáo nghiên cứu). Nếu là một Agent về lĩnh vực “Smart House”, có thể sẽ cần các hàm như: bật/tắt đèn, bật/tắt điều hòa, lấy thông tin môi trường, v.v. Việc triển khai hàm ở đây sẽ không được giải thích chi tiết. Một hàm có thể được viết code tự thực hiện, hoặc có thể được thực hiện qua việc gọi API bên ngoài.

Giả sử hàm của bạn đã được triển khai, chúng ta cần mô tả hàm này cho LLM. Các yếu tố cần thiết để mô tả một hàm bao gồm:

  • function name
  • function description
  • Function request parameter description
  • Response parameter description of the function (optional)

Ví dụ với hàm get_n_day_weather_forecast :

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_n_day_weather_forecast",
            "description": "Get the weather forecast for the next n days",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "City or district, e.g., Nanshan District, Shenzhen",
                    },
                    "format": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "The temperature unit to be used, Celsius or Fahrenheit",
                    },
                    "num_days": {
                        "type": "integer",
                        "description": "Number of forecast days",
                    }
                },
                "required": ["location", "format", "num_days"]
            },
        }
    }
]

Gọi LLM để nhận tham số yêu cầu hàm

Function Calling được thực hiện thông qua việc yêu cầu LLM thông qua chat API. Trong các tham số của chat API của các mô hình hỗ trợ Function Calling, sẽ có một tham số functions (hoặc tools, tùy theo LLM mà tham số có thể khác nhau). Bằng cách truyền tham số này, LLM sẽ biết được những hàm nào có sẵn để sử dụng. Đồng thời, nó sẽ dựa trên đầu vào của người dùng để suy luận xem nên gọi hàm nào, và chuyển ngữ cảnh tự nhiên thành tham số yêu cầu của hàm, trả về cho bên yêu cầu. Dưới đây là ví dụ sử dụng SDK của OpenAI:

from openai import OpenAI

def chat_completion_request(messages, tools=None, tool_choice=None, model="gpt-3.5-turbo"):
    try:
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            tools=tools,
            tool_choice=tool_choice,
        )
        return response
    except Exception as e:
        print("Unable to generate ChatCompletion response")
        print(f"Exception: {e}")
        return e


if __name__ == "__main__":
    messages = []
    messages.append({"role": "system", "content": "Do not assume which values will be input into the functions. If the user's request is unclear, ask for clarification"})
    messages.append({"role": "user", "content": "What is the weather like in Nanshan, Shenzhen for the next 5 days?"})
    chat_response = chat_completion_request(
        messages, tools=tools
    )

 tool_calls = chat_response.choices[0].message.tool_calls
 print("===Response===")
 print(tool_calls)

LLM sẽ trả về các tham số gọi của hàm: get_n_day_weather_forecast:

=== Response ===
[ChatCompletionMessageToolCall(id='call_7qGdyUEWp34ihubinIUCTXyH', function=Function(arguments='{"location":"Ha Noi","format":"celsius","num_days":5}', name='get_n_day_weather_forecast'), type='function')]

// Format as follows: chat_response.choices[0].message.tool_calls:
[
  {
    "id": "call_7qGdyUEWp34ihubinIUCTXyH",
    "function": {
      "arguments": {
        "location": "Ha Noi",
        "format": "celsius",
        "num_days": 5
      },
      "name": "get_n_day_weather_forecast"
    },
    "type": "function"
  }
]

Gọi hàm

Bên gọi nhận thông tin gọi hàm được trả về từ LLM (function name and call parameters), sau đó tự mình gọi hàm và nhận phản hồi từ việc thực thi hàm. Nếu cần, có thể bổ sung phản hồi từ việc thực thi hàm vào cuộc trò chuyện API và gửi lại cho LLM, để LLM sắp xếp thành câu trả lời tự nhiên cho người dùng.

# Execute functions
for tool_call in tool_calls:
    function = tool_call.function.name
    arguments_list = json.loads(tool_call.function.arguments)
    function_to_call = globals().get(function)
    result = function_to_call(**arguments_list)
    print("===" + function + "===")
    print(result)

    # Add the function call result to the conversation history
    messages.append(
        {
            "tool_call_id": tool_call.id,  # Used to identify the function call ID
            "role": "user",
            "name": function,
            "content": "Function execution result: " + str(result)
        }
    )
# Pass the function execution result to LLM to organize it into a natural language reply for the user
chat_response = chat_completion_request(
    messages, tools=tools
)
print("===Reply===")
print(chat_response.choices[0].message.content)

Kết quả:

[
    {"date": "2023-04-01", "location": "Ha Noi", "temperature": "20°C", "description": "Clear"},
    {"date": "2023-04-02", "location": "Ha Noi", "temperature": "21°C", "description": "Cloudy"},
    {"date": "2023-04-03", "location": "Ha Noi", "temperature": "22°C", "description": "Clear"},
    {"date": "2023-04-04", "location": "Ha Noi", "temperature": "23°C", "description": "Cloudy"},
    {"date": "2023-04-05", "location": "Ha Noi", "temperature": "24°C", "description": "Clear"}
]

REFERENCE

ReAct: Synergizing Reasoning and Acting in Language Models

LLM Agents MOOC

LLM Agent


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí