# Tích hợp AI agent on-device có tool calling vào app Android và iOS

[Philipp Lackner](https://homus.dev/speakers/philipp-lackner) · [Philipp Lackner](https://homus.dev/orgs/philipp-lackner)

> Philipp Lackner dựng một app to-do Kotlin Multiplatform có AI agent chạy offline bằng Gemma 4 và LiteRT-LM, với tool calling thật trên Android và iOS.

Topics: Agents, Dev Tools

Canonical: https://homus.dev/talks/beginners-guide-to-integrating-on-device-ai-agents-in-your-mobile-app-android-ios

## 1. AI agent on-device trong một app Kotlin Multiplatform

Philipp Lackner mở đầu bằng chủ đề của buổi hôm nay: AI agent chạy on-device trên thiết bị di động, cụ thể là trên iOS và Android, với app được viết bằng Kotlin Multiplatform. Theo lời anh (video đăng ngày 25/10/2026), đến cuối video người xem sẽ thấy cách mọi thứ hoạt động, khi nào cách làm này hữu ích, cách implement nó, và các phần code được nối với nhau ra sao. Anh đã chuẩn bị sẵn project, nhưng mục tiêu là để người xem hiểu được các mảnh ghép nối với nhau thế nào, rồi tự implement được trong code base của chính mình.

![Philipp Lackner nói trước camera, góc trên bên trái có tên PHILIPP LACKNER](https://homus.dev/photos/fW1wKSBDBHA-0003.jpg)

Philipp Lackner ở phần mở đầu video (đăng ngày 25/10/2026), với dòng tên của anh ở góc trên bên trái.

Nội dung chính, theo Philipp (video đăng ngày 25/10/2026), là làm sao để thật sự có những AI agent chạy on-device như vậy trên một thiết bị Android, dùng model của riêng bạn, có thể là model tải về, hoặc là system model có sẵn trong hệ điều hành, và có cả tool calling thật sự với những model đó.

Để minh hoạ, anh dùng một ứng dụng to-do list (demo trong video đăng ngày 25/10/2026). Ở mức bình thường, bạn có thể tick các to-do như mọi app khác. Các to-do được sắp xếp theo ngày. Bạn có thể sửa và xoá chúng, vân vân. Phần khác biệt nằm ở chỗ một AI agent chạy on-device có thể giúp gì thêm: bạn có thể tự định nghĩa các tool của riêng mình.

## 2. Tool là gì, và demo tạo to-do khi đang ở chế độ máy bay

Với những ai chưa quen khái niệm, Philipp giải thích (video đăng ngày 25/10/2026): trong thế giới AI, một tool về cơ bản là một hành động, một việc mà AI agent có thể thực thi như một phần của câu trả lời. Trên màn hình hiện một thẻ định nghĩa "Tool in agentic AI".

![Thẻ chữ Tool in agentic AI trên nền đen](https://homus.dev/photos/fW1wKSBDBHA-0058.jpg)

Thẻ định nghĩa trong video (đăng ngày 25/10/2026): một tool là một hàm, một API hay một interface có thể thực thi, được đưa cho Large Language Model (LLM) để nó tương tác với thế giới bên ngoài và làm những việc vượt ra ngoài việc chỉ sinh ra văn bản.

Trong app này (demo trong video đăng ngày 25/10/2026), các to-do được lưu trong một database Room cục bộ trên máy. Từ đó, bạn có thể định nghĩa những hành động như truy vấn dữ liệu hoặc tìm kiếm trong danh sách to-do. Bạn cũng có thể có một hành động tạo to-do, một hành động đánh dấu to-do là đã xong. Mỗi hành động như vậy là một tool call riêng mà bạn định nghĩa cho AI agent, để nó gọi tới khi, dựa trên prompt của bạn, nó cho rằng đây là hành động nên thực hiện.

Philipp mở khung chat được tích hợp sẵn trong app (khung "Assistant", dòng phụ ghi "Runs fully on your device") và gõ yêu cầu tạo một to-do để xem video mới nhất của Philipp, vào thứ Năm (demo trong video đăng ngày 25/10/2026). Câu trên màn hình:

```
Create a todo to watch Philipp's latest video on Thursday
```

![Màn hình Android với khung Assistant, ô nhập đang chứa câu Create a todo to watch Philipp's latest video on Thursday](https://homus.dev/photos/fW1wKSBDBHA-0143.jpg)

Khung chat Assistant trên Android trong demo (video đăng ngày 25/10/2026): biểu tượng máy bay ở thanh trạng thái cho thấy máy đang ở chế độ máy bay, và câu lệnh tạo to-do đang nằm trong ô nhập, chuẩn bị gửi.

Anh chỉ ra rằng máy đang offline, đang bật chế độ máy bay, vì đây là một model chạy on-device; nó hoạt động khi offline (demo trong video đăng ngày 25/10/2026). Model "suy nghĩ" một lúc rồi trả lời. Trong khung chat hiện ra tool đã được gọi: `create_todo`, với ngày thứ Năm và tiêu đề "watch Philipp's latest video". Model báo lại rằng nó đã tạo to-do này cho thứ Năm.

Quay lại danh sách to-do, to-do mới nằm ở đó: "watch Philipp's latest video", hạn ngày 22 tháng 10. Philipp nói lúc quay là ngày 20 tháng 10, và to-do được thêm vào ngay lúc đó (demo trong video đăng ngày 25/10/2026). Toàn bộ việc này chạy hoàn toàn offline, không cần API key nào, không cần nhà cung cấp bên ngoài nào, chỉ có model đang chạy cục bộ trên chiếc điện thoại Android này.

## 3. Cùng app trên iOS, và agent là một LLM chạy trong vòng lặp

Theo Philipp (video đăng ngày 25/10/2026), cách làm này cũng chạy trên iOS. Anh có cùng app đó trên iOS, với cùng kiểu to-do. Anh mở chat và gõ rằng mình vừa uống nước, nên hãy đánh dấu to-do đó là đã xong. Câu trên màn hình iPhone:

```
I just drank water so Mark this todo as done
```

![iPhone với khung Assistant: tin nhắn I just drank water so Mark this todo as done, hai tool call search_todos và set_todo_done, và câu trả lời](https://homus.dev/photos/fW1wKSBDBHA-0403.jpg)

Demo trên iOS (video đăng ngày 25/10/2026): sau câu "I just drank water so Mark this todo as done", agent gọi `search_todos` với "drank water", rồi `set_todo_done` với "Drink some water", và trả lời rằng nó đã đánh dấu to-do "Drink some water" là xong. Ô nhập phía dưới đang được gõ prompt tiếp theo.

Gửi đi rồi xem lại to-do "Drink some water", bạn thấy AI agent đã thật sự đánh dấu nó là đã xong (demo trong video đăng ngày 25/10/2026).

Từ đây Philipp đi từng bước một để giải thích mọi thứ hoạt động ra sao, vì có thể việc làm việc với AI agent, thật sự tích hợp chúng vào app hay sản phẩm của bạn, là điều mới với bạn. Câu hỏi đầu tiên: rốt cuộc AI agent là gì? Demo nhỏ vừa rồi đã cho một chút bối cảnh. Nhưng tới thời điểm này, theo anh (video đăng ngày 25/10/2026), nó chưa khác lắm so với việc mở ChatGPT rồi gõ một prompt nào đó.

Điểm khác, theo Philipp (video đăng ngày 25/10/2026), là AI agent bằng cách nào đó điều khiển được ứng dụng của bạn: nó thực thi được hành động, truy vấn được database, tìm kiếm to-do, hay đánh dấu to-do là đã xong. Đó là một điều làm AI agent trở nên đặc biệt. Điều thứ hai là AI agent chạy trong một vòng lặp.

Và về cơ bản, theo anh (video đăng ngày 25/10/2026), AI agent chính là như vậy: một LLM chạy trong vòng lặp, trong đó agent có thể tự kiểm tra output của chính nó. Nếu nó cho rằng output đó chưa đạt tới điều người dùng thật sự muốn, nó có thể tự prompt lại chính mình và đi tiếp qua các hành động khác. Điều này có nghĩa là một prompt duy nhất có thể dẫn tới nhiều tool call.

Định nghĩa agent Philipp đưa ra trong video (đăng ngày 25/10/2026): một LLM chạy trong vòng lặp, gọi tool, xem kết quả, và tự đi tiếp tới khi cho rằng đã đạt điều người dùng muốn.

## 4. Hai việc trong một prompt, và giới hạn của model on-device

Để cho thấy điều đó, Philipp mở lại chat và gõ yêu cầu thêm một to-do mới "Drink some wine", rồi tick to-do "eating vegetables" là đã xong (demo trong video đăng ngày 25/10/2026). Giữa chừng anh nói tính năng autocomplete thật phiền. Câu cuối cùng trên màn hình:

```
Add a new todo to Drink some wine and then tick the todo with eating vegetables as done
```

Như vậy có hai hành động trong một prompt. Ở prompt trước, bạn đã thấy agent tìm to-do theo "drinking water" rồi đánh dấu nó là xong. Còn bây giờ là hai hành động độc lập với nhau, và theo Philipp (video đăng ngày 25/10/2026), AI agent hỗ trợ việc gọi nhiều tool sau một prompt duy nhất, trong một câu trả lời duy nhất của LLM.

![Hai điện thoại: danh sách to-do trên Android và khung Assistant trên iPhone với hai tool call create_todo và set_todo_done](https://homus.dev/photos/fW1wKSBDBHA-0443.jpg)

Kết quả của prompt hai việc (demo trong video đăng ngày 25/10/2026): agent gọi `create_todo` với "Drink some wine" và `set_todo_done` với "eating vegetables". Nó trả lời rằng đã thêm to-do "Drink some wine", nhưng không tìm thấy to-do nào có tiêu đề "eating vegetables", và hỏi lại tiêu đề chính xác.

Nhưng ngay ở đây, Philipp chỉ ra, bạn đã thấy được vài giới hạn của các model on-device này. Theo anh (video đăng ngày 25/10/2026), bản thân các LLM chạy local vốn đã khá yếu, tuỳ phần cứng bạn chạy chúng. Còn nếu chạy trên thiết bị di động, bạn càng bị giới hạn về phần cứng hơn nữa.

Cụ thể (demo trong video đăng ngày 25/10/2026): dù trong danh sách có một to-do tên là "Eat some vegetables", model không hiểu prompt theo hướng đi tìm to-do đó. Nó cho rằng có một to-do tên chính xác là "eating vegetables", và không tìm thấy.

Đó đơn giản là một giới hạn. Theo Philipp (video đăng ngày 25/10/2026), bạn thường phải chấp nhận rằng AI chạy local, nhất là trên mobile, không giỏi lắm với những việc phức tạp.

## 5. Ngày tương đối, và lúc nào nên nghĩ tới model trên cloud

Một thứ khác cũng từng là khó khăn trong ứng dụng này, theo Philipp (video đăng ngày 25/10/2026), là làm cho AI chạy local hiểu và xử lý được ngày tương đối. Chẳng hạn, nếu bạn nói "sửa to-do cho thứ Ba tuần sau", thì lúc đầu việc đó không chạy nếu không có một cách lách: anh luôn phải đưa ngày hiện tại vào system prompt được nạp cho model. Nhưng từng bước một; anh hẹn sẽ quay lại chuyện này.

Tới đây, theo anh, bạn đã có hình dung sơ bộ về cách AI agent hoạt động và lúc nào thì có nó trong ứng dụng là hữu ích. Thật ra, theo Philipp (video đăng ngày 25/10/2026), bất kỳ hành động nào, bất kỳ đoạn code nào bạn định nghĩa được trong app, bạn cũng có thể cho chạy như kết quả của một LLM agent chạy local như thế này.

Nếu bạn cần đánh giá prompt phức tạp hơn, thì có thể bạn không muốn dùng một LLM on-device. Bạn đã thấy rồi, nó không phải lúc nào cũng cho câu trả lời hoàn hảo, vì theo Philipp (video đăng ngày 25/10/2026) các model LLM chạy local này "cực kỳ đơn giản". Trong những trường hợp đó, bạn tất nhiên có thể nghĩ tới việc dùng các model mạnh hơn chạy trên cloud.

Ví dụ, trên Android, theo Philipp (video đăng ngày 25/10/2026), bạn cũng có thể dùng hybrid inference của Firebase. Về cơ bản đây là một cách để linh hoạt quyết định một prompt cụ thể nên chạy trên model nào: một LLM đọc prompt của bạn rồi nói "prompt này quá phức tạp, nó nên chạy trên các model cloud mạnh hơn", hoặc "nó đủ đơn giản, mình có thể chạy trên model local".

![Trang tài liệu Build hybrid and on-device experiences with Firebase AI Logic, một câu được bôi đậm](https://homus.dev/photos/fW1wKSBDBHA-0617.jpg)

Trang tài liệu Firebase AI Logic hiện trong video (đăng ngày 25/10/2026), với câu được bôi nổi: hybrid inference cho phép chạy inference bằng model on-device khi có sẵn, và chuyển sang model trên cloud khi không có (và cả chiều ngược lại).

Với model local, dữ liệu không bao giờ rời khỏi thiết bị. Theo Philipp (video đăng ngày 25/10/2026), đó tất nhiên là một trong những lợi thế lớn của các model local: dữ liệu được giữ riêng tư, không bao giờ bị gửi đi đâu. Bạn cũng không cần kết nối internet để chạy chúng, nhưng đó là những use case rất đơn giản.

Ở phía bên kia, các model trên cloud phức tạp hơn nhiều, vì trên cloud bạn có tài nguyên phần cứng về cơ bản là không giới hạn, theo lời anh (video đăng ngày 25/10/2026). Nhưng điều đó cũng có nghĩa là dữ liệu phải rời khỏi thiết bị: bạn phải gửi thứ gì đó tới, có thể là server của Google, server của Anthropic, hay bất kỳ nhà cung cấp model nào bạn đang dùng. Nó cũng có nghĩa là bạn phải trả tiền, vì theo anh, họ không làm việc này miễn phí, họ không xây tất cả những data center đó miễn phí. Dù vậy, anh quay lại tập trung vào AI chạy local, vì theo anh nó thật sự thú vị với nhiều người xem.

## 6. Chọn model: system model của Android và iOS

Vậy làm sao để thực sự làm được việc này? Cụ thể, thứ Philipp có ở đây (video đăng ngày 25/10/2026) là một ứng dụng Kotlin Multiplatform với UI viết bằng Compose Multiplatform. Nhưng anh không muốn tập trung vào phần UI, vì mục tiêu là xem các AI agent này hoạt động thế nào và xem toàn bộ phần code.

Điều đầu tiên cần quyết định, theo Philipp, là bạn thật sự muốn dùng loại model nào. Có nhiều lựa chọn. Theo anh (video đăng ngày 25/10/2026), trên các thiết bị Android và iOS đời mới luôn có sẵn một system model: trên Android là Gemini Nano, còn trên iOS là Apple Foundation Models.

Dùng system model có lợi thế là chúng sẵn sàng ngay lập tức. Theo Philipp (video đăng ngày 25/10/2026), chúng được tích hợp sẵn vào hệ điều hành, không cần tải model về.

Nhưng ít nhất trên Android, tính đến lúc quay video (đăng ngày 25/10/2026), theo Philipp, các system model không có native tool calling. Trong model Gemini Nano không có gì được implement để nó trả lời bằng một danh sách tool call mà nó muốn gọi, như tạo một to-do hay tìm một to-do cụ thể. Vì vậy, nếu muốn có khả năng tool calling trong app Android, bạn không dựa được vào system model này, hoặc bạn phải tự implement.

Tự implement cũng là một lựa chọn, theo anh (video đăng ngày 25/10/2026): bạn chỉ việc nói "này model thân mến, hãy luôn trả lời theo đúng định dạng JSON này và cho tôi tên các tool bạn sẽ gọi". Bạn có thể lách theo cách đó. Nhưng khi ấy nó không còn là native tool calling được tích hợp vào model nữa, thứ mà các công ty lớn đã kiểm thử kỹ để đảm bảo đúng tool được gọi vào đúng lúc. Trong trường hợp tự làm, điều đó sẽ không có.

Ba lựa chọn Philipp nêu trong video (đăng ngày 25/10/2026), với ưu và nhược điểm anh nói cho từng cái; project demo dùng lựa chọn ở giữa.

## 7. Tự tải Gemma 4 và chạy bằng LiteRT-LM

Lựa chọn thứ hai, và là lựa chọn Philipp đã chọn cho project này (video đăng ngày 25/10/2026), là thật sự tải một model về để dùng local, với native tool calling được hỗ trợ sẵn bên trong model. Cụ thể, trên thiết bị này anh dùng model Gemma 4, chạy bằng runtime LiteRT-LM. Theo anh, đó về cơ bản là một framework open source của Google, đóng vai trò runtime cho các model on-device.

Như vậy bạn lấy model, ở đây là Gemma 4, và tải nó về; trong trường hợp của Philipp là tải từ Hugging Face (video đăng ngày 25/10/2026). Anh mô tả Hugging Face đơn giản là một kho lớn chứa các model open source.

![Trang Hugging Face, collection Gemma 4 của Google với danh sách các model gemma-4](https://homus.dev/photos/fW1wKSBDBHA-0918.jpg)

Collection Gemma 4 của Google trên Hugging Face như hiện trong video (đăng ngày 25/10/2026): các bản gemma-4-31B, gemma-4-26B-A4B và gemma-4-E4B, mỗi bản có cả phiên bản "-it".

Sau đó model được lưu trong bộ nhớ trong của app Android và iOS để dùng cho inference: gửi prompt vào và nhận lại output, hay câu trả lời, nhưng cũng nhận lại một danh sách tool call, vì model này hỗ trợ chúng (theo Philipp, video đăng ngày 25/10/2026).

Cách này cũng có nhược điểm. Thứ nhất, bạn cần tải model về, mà theo Philipp, ở đây model nặng khoảng 2,5 gigabyte, "tôi nghĩ vậy" (video đăng ngày 25/10/2026). Trên một thiết bị di động, con số đó có thể là khá nhiều. File `LocalModel.kt` trong repo của project ghi model là Gemma 4 E2B, kích thước 2.588.147.712 byte (đọc ngày 31/10/2026).

Thứ hai, vì đây không phải model dùng chung toàn hệ thống, nếu app của bạn tải nó về và một app khác cũng cần nó, thì về mặt kỹ thuật app kia cũng phải tự tải model về, trừ khi có một hạ tầng phức tạp hơn để chia sẻ (theo Philipp, video đăng ngày 25/10/2026). Thứ ba, tính đến lúc quay video, Swift API cho runtime LiteRT vẫn đang ở giai đoạn early preview rất sớm.

Philipp đã làm cho nó chạy được trong project này (demo trong video đăng ngày 25/10/2026), nên app cũng chạy được trên iOS, vì có một phần native code tham gia để model chạy được trên cả Android lẫn iOS. Anh build theo cách để phần lớn code được dùng chung trong project, nhưng vẫn có vài phần native. Và trên iOS, phần này vẫn đang ở early preview rất sớm, nên đó là điều cần lưu ý, theo lời anh.

## 8. Cách làm lai hai nền tảng, và semantic search nằm ngoài phạm vi

Về mặt kỹ thuật, theo Philipp (video đăng ngày 25/10/2026), bạn cũng có thể chọn một cách làm pha trộn hơn, kiểu hybrid. Bạn nói: được rồi, trên Android mình dùng runtime LiteRT-LM, vốn không phải early preview trên Android. Bạn tải model về trên Android, nhưng trên iOS thì dựa vào Apple Foundation Models, tức các system model, vì theo anh, trên iOS chúng có hỗ trợ native tool calling.

Theo cách đó, bạn có native tool calling tích hợp trong model, và chỉ cần tải model trên Android. Nhưng theo Philipp (video đăng ngày 25/10/2026), điều đó cũng có nhược điểm: về mặt kỹ thuật bạn không còn dùng cùng một model nữa. Vì vậy bạn có thể sẽ thấy khác biệt, ít nhất là ở cách chat và agent hành xử trên hai nền tảng.

Anh gọi đây là vài gợi ý để suy nghĩ, phòng khi bạn cần xây thứ gì đó tương tự. Theo anh (video đăng ngày 25/10/2026), không có một đáp án đúng duy nhất ở đây; tất cả thật sự tuỳ vào thứ bạn đang xây.

Rồi anh chuyển sang nhìn kỹ hơn về mặt kỹ thuật. Trước tiên, có một thứ chắc chắn nằm ngoài phạm vi project này: semantic search. Semantic search, theo Philipp (video đăng ngày 25/10/2026), về cơ bản là truy vấn database dựa trên ngữ nghĩa. Bạn có thể hỏi: có việc nào mà nếu không làm thì tôi sẽ gặp rắc rối pháp lý lớn không? Ví dụ như không trả tiền thuê nhà.

Ví dụ semantic search Philipp đưa ra trong video (đăng ngày 25/10/2026): model trong app chưa hiểu được câu hỏi kiểu này, còn một hệ RAG thì có thể, nhưng anh để nó ngoài phạm vi.

Theo Philipp (video đăng ngày 25/10/2026), model hiện tại sẽ không hiểu câu đó. Có thể nó sẽ tìm từ "trouble" hay đại loại vậy, nhưng rõ ràng bạn sẽ không tìm thấy gì, vì thứ nó thật sự cần là hiểu được về mặt ngữ nghĩa rằng không trả tiền nhà nghĩa là bạn sẽ gặp rắc rối. Bạn cũng có thể implement điều này, khi đó nó sẽ là một hệ RAG, retrieval augmented generation. Đó là một thứ phức tạp hơn, nên nằm ngoài phạm vi của bản implement này.

## 9. Đưa LiteRT-LM vào app iOS và Android

Giờ tới code base. Trước hết, theo Philipp (video đăng ngày 25/10/2026), cần hiểu model trên iOS, tức Gemma 4 trên iOS, thật sự đi vào app iOS bằng cách nào. Để làm việc đó, trong repo có một Gradle task. Anh tìm một chút ("nó đâu rồi... đây") rồi mở ra: một Gradle task về cơ bản tải runtime LiteRT-LM từ GitHub về dưới dạng một file zip, cho iOS framework. Theo anh, một framework trên iOS thật ra là một dạng thư viện.

![File build.gradle.kts với task downloadLiteRtLmFramework tải CLiteRTLM.xcframework.zip từ GitHub rồi unzip](https://homus.dev/photos/fW1wKSBDBHA-1223.jpg)

Gradle task `downloadLiteRtLmFramework` trong `build.gradle.kts` của module shared, như trên màn hình (video đăng ngày 25/10/2026): nó dựng URL release của google-ai-edge/LiteRT-LM trên GitHub theo version, tải `CLiteRTLM.xcframework.zip`, rồi gọi `unzip` và báo lỗi nếu giải nén thất bại.

Bạn cần tải framework đó về, và Gradle task này cũng giải nén nó. Sau đó, vì framework này có native code, cụ thể là code C ở phía iOS, theo Philipp (video đăng ngày 25/10/2026) bạn còn cần các package native interop và C interop ở đây, nơi bạn chỉ việc khai báo để nó làm việc với Objective-C. Ở thời điểm này, anh chỉ muốn người xem biết rằng để đưa framework này vào, phía iOS cần phần native code đó.

Với Android thì dễ hơn thật sự, theo Philipp (video đăng ngày 25/10/2026). Trên Android, bạn chỉ có một tham chiếu trong version catalog tới LiteRT-LM, được thêm vào dependency của `androidMain`. File `libs.versions.toml` trong repo khai báo thư viện `com.google.ai.edge.litertlm:litertlm-android`, version 0.18.0 (đọc ngày 31/10/2026).

Rồi anh vào xem code. Có các file chat, gồm phần UI. Có `ChatViewModel`. Theo Philipp (video đăng ngày 25/10/2026), đây là điểm kích hoạt ban đầu, vì view model quản lý UI state, ở đây là ô nhập text và toàn bộ state của màn hình.

## 10. Từ ChatViewModel tới TodoAgent

Khi bạn gõ gì đó vào ô nhập rồi bấm gửi, theo Philipp (video đăng ngày 25/10/2026), code trước tiên chạy qua view model, cụ thể là hàm `sendMessage`. Ở đó có một generation job, thứ sinh ra câu trả lời, và nó gọi hàm `send` của một agent. Agent này về cơ bản là một instance, như bạn cũng thấy trên màn hình, của một `TodoAgent` được viết riêng.

`TodoAgent` này, theo Philipp (video đăng ngày 25/10/2026), có một LLM engine thực sự chạy inference. Nó nhận một danh sách các tool call có thể được gọi khi một prompt được gửi vào agent. Ở đây đó có thể là tìm to-do, lấy toàn bộ to-do, tạo một to-do, đánh dấu một to-do là xong.

Agent sau đó chỉ giữ một instance của một LLM conversation, mà theo anh là tương đương một cửa sổ chat. Trong ví dụ này chỉ có một conversation như vậy, nhưng rõ ràng bạn có thể có một danh sách và có nhiều phiên chat (theo Philipp, video đăng ngày 25/10/2026).

![File TodoAgent.kt: hàm send(prompt) với mutex.withLock, vòng while trên response.toolCalls và MAX_TOOL_ROUNDS](https://homus.dev/photos/fW1wKSBDBHA-1423.jpg)

Hàm `send(prompt: String): Flow<AgentEvent>` của `TodoAgent` trên màn hình (video đăng ngày 25/10/2026): khoá bằng `mutex.withLock`, lấy conversation hiện tại, gửi `LlmMessage.User`, rồi lặp chừng nào `response.toolCalls` còn khác rỗng và `round` còn nhỏ hơn `MAX_TOOL_ROUNDS`; cuối cùng emit `AgentEvent.Answer`.

Khi bạn gửi một prompt, chỉ là một chuỗi string thuần, vào agent này thì chuyện gì xảy ra? Theo Philipp (video đăng ngày 25/10/2026), trước hết là một mutex để xử lý race condition, vì code thao tác trên các object dùng chung. Rồi nó lấy conversation hiện tại, hoặc tạo một conversation mới nếu vẫn còn null. Sau đó nó gửi prompt đi dưới dạng một user prompt.

Ở đây có kiểu `LlmMessage`. Theo Philipp (video đăng ngày 25/10/2026), đây chỉ là một gợi ý cho cách bạn có thể mô hình hoá thứ này về mặt kiến trúc. Một `LlmMessage` có thể là `User`, hoặc là một danh sách kết quả tool (`ToolResults`). `User` chính là user prompt, như bạn thấy ở đây: code dựng một user prompt từ chuỗi prompt được truyền vào hàm.

Rồi nó gửi message đó vào conversation, thứ được tạo ra bởi hàm `currentConversation`. Nhìn vào hàm đó, theo Philipp (video đăng ngày 25/10/2026), nó chỉ là một lớp abstraction, vì cách cụ thể, hay runtime cụ thể bạn muốn dùng, có thể thay đổi.

## 11. Engine, conversation và LlmResponse

Bên dưới lớp abstraction đó, theo Philipp (video đăng ngày 25/10/2026), là framework LiteRT-LM làm runtime để thực sự xử lý prompt. Bản implement cụ thể đến từ `LiteRtLmEngine`, nơi nạp model rồi tạo engine. Anh tìm một chút rồi chỉ ra: tạo engine về cơ bản chỉ là một loạt config, như đường dẫn model, backend, số token tối đa, context window, vân vân. Bạn tạo engine rồi cuối cùng nạp model.

Khi mở app lần đầu, theo Philipp (video đăng ngày 25/10/2026), có một khoảng trễ nhỏ: chừng hai, ba, bốn giây app chỉ nạp model, và sẽ có một vòng quay loading. Còn nếu bạn mở app lần đầu tiên hoàn toàn, nó sẽ tải model về. Anh hẹn sẽ cho xem phần đó ngay sau, và theo anh đó về cơ bản chỉ là một HTTP call để tải một thứ về.

Phần thú vị hơn, theo Philipp (video đăng ngày 25/10/2026), là một conversation như vậy, vì ở đây bạn thật sự gửi message vào conversation. Và conversation này giờ không còn là object của riêng bạn nữa, mà là một object đến từ LiteRT-LM. Đó là thứ thật sự xử lý cuộc hội thoại, gửi prompt "sâu vào trong AI", để nó xử lý và chạy inference.

Thứ hàm này trả về là một response, theo Philipp (video đăng ngày 25/10/2026). Response ở đây là một message, và code chỉ map nó sang định dạng riêng của mình để làm việc với các object ở tầng domain, ở đây là một `LlmResponse`. Trong project này, response gồm một đoạn text, tức chính message model trả lời, ví dụ câu "I've created the to-do for you to watch Philipp's YouTube video", và một danh sách tool call.

Các kiểu dữ liệu trong file `LlmMessage.kt` mà Philipp mở trong video (đăng ngày 25/10/2026): message gửi vào là `User` hoặc `ToolResults`; câu trả lời gồm text và danh sách `ToolCall`, mỗi cái có tên và arguments.

Model dựa trên prompt để cân nhắc, theo Philipp (video đăng ngày 25/10/2026): đây là các tool call có sẵn trong bối cảnh này, tạo to-do, đánh dấu to-do là xong, tìm to-do; với danh sách tool call này và với prompt kia, mình có cần gọi tool nào không? Về cơ bản đó là điều model đánh giá. Nếu nó cho rằng nên gọi một tool, danh sách tool call sẽ không rỗng. Nó sẽ chứa ít nhất một tool call, và có thể nhiều hơn.

Vì nếu bạn nói "tạo một to-do rồi đánh dấu nó là xong ngay", theo Philipp (video đăng ngày 25/10/2026), thì đó có thể là hai tool call riêng rẽ mà model trả về. Khi đó code phải duyệt qua chúng: nếu đây là lời gọi tạo to-do, thì đây là đoạn code cần chạy, đây là câu truy vấn database cần chạy. Về cơ bản, theo anh, mọi thứ hoạt động như vậy.

## 12. Vòng lặp tool call và giới hạn số vòng

Quay lại agent (video đăng ngày 25/10/2026): ban đầu code gửi prompt vào conversation, và cuối cùng nhận lại response ở định dạng `LlmResponse`. Việc tiếp theo, theo Philipp, là kiểm tra xem danh sách tool call có thật sự khác rỗng không: có tool call nào mà LLM nói "đây là thứ bạn cần gọi bây giờ" không?

Code cũng có một giới hạn số vòng tool tối đa được định nghĩa ở đây. Theo Philipp (video đăng ngày 25/10/2026), nếu LLM trả về cả nghìn tool call, thì nên giới hạn ở mức khoảng năm hay đại loại vậy. Trên màn hình, hằng số `MAX_TOOL_ROUNDS` trong `TodoAgent` được đặt bằng 5.

Nếu các điều kiện đó thoả, code duyệt qua các tool call. Với mỗi cái, nó emit tool call đó; theo Philipp (video đăng ngày 25/10/2026), hàm này trả về một flow, nên nó báo ra: ở đúng thời điểm này, mình đang thực thi tool call này. Đó là thứ có thể quan sát ở tầng view model để hiện ra những "chip" tool call nhỏ trong khung chat. Nhờ vậy bạn thấy theo thời gian thực lúc một tool call đang chạy.

Code thu được một danh sách kết quả tool, mỗi kết quả là kết quả của việc gọi `execute` trên tool call đó. `execute` là gì? Theo Philipp (video đăng ngày 25/10/2026), về cơ bản nó là một hàm nhận vào một object tool call, và tool call ở đây chỉ là một cái tên cùng một loạt arguments, ví dụ tiêu đề của to-do sắp được tạo, rồi chạy nó.

Luồng của hàm `send` trong `TodoAgent` theo code trên màn hình (video đăng ngày 25/10/2026): mỗi vòng chạy các tool call mà LLM chọn, gửi kết quả lại cho LLM, tối đa 5 vòng, rồi emit một câu trả lời.

Đi vào `execute`, bạn thấy lớp abstraction cho một agent tool, `AgentTool`. Theo Philipp (video đăng ngày 25/10/2026), lý do là bạn muốn mở rộng được để định nghĩa đủ loại tool, không chỉ để tạo to-do, mà có thể cả khi mở rộng app để agent đảm nhận các phần khác của project. Vì có abstraction này, khi muốn xây một agent tool cụ thể, như tạo to-do ở đây, bạn chỉ việc implement nó.

## 13. AgentTool: mỗi tool một class, ví dụ CreateTodoTool

Trên màn hình là một loạt tool đã được định nghĩa (video đăng ngày 25/10/2026). Theo Philipp, đây đúng là kiểu kiến trúc anh đề xuất, vì nó thật sự dễ mở rộng: ngay khi bạn muốn có thêm một tool, bạn chỉ việc thêm một class cho đúng tool đó. Class đó implement interface này, và có các dependency của nó. Ví dụ, tool tạo to-do cần làm việc với local data source để tạo được to-do.

Nó có thể cũng cần truy cập thời gian, vì có thể bạn muốn tạo to-do dựa trên một ngày tương đối. Như Philipp đã nhắc, theo anh (video đăng ngày 25/10/2026), các model local này khá tệ khoản tính ngày. Nếu bạn nói "cho tôi một to-do sau 12 ngày nữa", thì đó là việc mà các model yếu này làm kém.

Vì vậy, với kiểu tính toán ngày như thế, thứ khá phổ biến trong loại app này ("tạo cho tôi một việc vào ngày cụ thể này"), theo Philipp (video đăng ngày 25/10/2026), bạn muốn tự tính ngày đó bằng code, rồi đưa ngày đã tính cho LLM để nó biết cần tạo việc cho ngày nào.

Rốt cuộc, theo Philipp (video đăng ngày 25/10/2026), một tool như vậy gồm một cái tên rõ ràng, và một description để LLM biết khi nào nên gọi tool này. Nếu LLM thấy "à, người dùng đang yêu cầu tạo một to-do mới", thì yêu cầu đó khớp với description của tool này. Rồi có một loạt tham số, mỗi tham số có tên, kiểu dữ liệu, và mô tả tham số đó dùng để làm gì, để LLM cũng hiểu được.

![File CreateTodoTool.kt: danh sách parameters với title, description và due, mỗi cái có type STRING và description](https://homus.dev/photos/fW1wKSBDBHA-2043.jpg)

Phần `parameters` của `CreateTodoTool` trên màn hình (video đăng ngày 25/10/2026): `title` ("Short title of the to-do."), `description` ("Optional extra details.", không bắt buộc) và `due`, với description dặn chép đúng lời người dùng mà không tự chuyển đổi, kèm ví dụ lấy từ `DateExpressions.DATE_EXAMPLES`.

Và tool có một hàm `execute`, theo Philipp (video đăng ngày 25/10/2026), được gọi ngay khi agent thật sự gọi tới tool này. Trong trường hợp này, code chỉ parse các arguments, lấy thời gian hiện tại, tạo to-do với những arguments đó, mà các arguments này do LLM truyền vào hàm. Rồi code chỉ đơn giản upsert to-do đó vào database (trên màn hình là `todoDataSource.upsertTodo(todo)`), và trả về vài kết quả: to-do đã được tạo hay chưa, và to-do nào đã được tạo, tức một chút thông tin.

Response output này sau đó được đưa ngược vào LLM, theo Philipp (video đăng ngày 25/10/2026), để nó đọc kết quả và nói "đúng rồi, to-do đã được tạo thành công". Về cơ bản, theo anh, mọi thứ hoạt động như vậy.

Philipp gợi ý bạn có thể nghĩ về các tool call này như các use case. Anh nói rõ anh không bảo đây là use case theo nghĩa của clean architecture, nhưng định dạng thì giống nhau: quy ước single responsibility được đáp ứng ở đây. Bạn chỉ việc nói: mình có một class cho mỗi tool call muốn định nghĩa trong project, mỗi class có một hàm `execute` duy nhất, nhận một loạt arguments và trả về một kết quả nào đó (theo Philipp, video đăng ngày 25/10/2026).

## 14. System prompt, quyền của agent và ngày hôm nay

Và đó, theo Philipp (video đăng ngày 25/10/2026), là cách nó hoạt động. Với tất cả các tool call mà LLM đã quyết định, code duyệt qua chúng, map sang các tool call, và thật sự chạy tool. Lần chạy đó cho ra một kết quả, kết quả này được trả về view model như kết quả của các tool call.

Đó là thứ cuối cùng, câu trả lời mà code emit ra. Theo Philipp (video đăng ngày 25/10/2026), luôn chỉ có một output cuối, một response, nhưng có thể gồm nhiều sự kiện tool call mà view model diễn giải rồi map sang bất kỳ kiểu UI nào bạn muốn. Tóm gọn, theo anh, các AI agent on-device hoạt động như vậy.

Theo Philipp (video đăng ngày 25/10/2026), chúng rất linh hoạt, vì việc chúng gọi những tool nào hoàn toàn do bạn định nghĩa. Bạn có thể cho chúng truy cập chính xác những phần của app mà bạn muốn chúng truy cập, và tinh chỉnh chúng theo đúng hướng bạn muốn.

Và các agent on-device này, cũng như bất kỳ LLM nào khác, có một system prompt mà bạn tự do định nghĩa, theo Philipp (video đăng ngày 25/10/2026). Đó về cơ bản là bối cảnh luôn nằm trong mọi phiên, bất kể bạn có khai báo rõ ràng trong phiên đó hay không: chỉ là vài bối cảnh cơ bản về mục đích của agent. Theo anh, ở đây bạn nên cẩn thận định nghĩa cho đúng: mục đích của agent là gì, nó nên giúp người dùng ra sao, nó có những quy tắc nào, những việc gì nó không nên làm.

Ví dụ, Philipp nói (video đăng ngày 25/10/2026) anh sẽ cẩn thận khi để một AI agent xoá thứ gì đó, nhất là một agent on-device, vốn có thể yếu ở khoản đánh giá xem việc xoá có hợp lý không. Nhưng đó là những thứ bạn định nghĩa được. Nếu không muốn AI agent có khả năng xoá, bạn đơn giản là không định nghĩa tool call cho việc đó. Khi ấy, theo anh, nó không thể làm việc đó, ngay từ thiết kế.

Một điểm đáng chú ý trong system prompt, theo Philipp (video đăng ngày 25/10/2026): anh thêm ngày hiện tại vào một cách động, vì anh nhận thấy nếu không làm vậy, model khá yếu khoản tính ngày tương đối. Nếu bạn nói "cho tôi một to-do vào Chủ nhật tuần sau", thì đôi khi nó tạo to-do vào sai ngày. Vì vậy anh đưa ngày hiện tại vào đây, và với tool tạo to-do, nếu có một ngày động, code sẽ thật sự tự tính ngày tương đối rồi đưa nó cho agent.

![Hàm buildSystemPrompt trong TodoAgent.kt với dòng Today is ${today.toSpokenDate()} và danh sách Rules](https://homus.dev/photos/fW1wKSBDBHA-2303.jpg)

Hàm `buildSystemPrompt(today: LocalDate)` trên màn hình (video đăng ngày 25/10/2026): dòng "Today is ${today.toSpokenDate()}" chèn ngày hôm nay vào prompt, theo sau là danh sách Rules; bên dưới là `MAX_TOOL_ROUNDS = 5`.

Trên màn hình, các dòng dài bị cắt ở mép phải. Bản đầy đủ của system prompt trong file `TodoAgent.kt` của repo (đọc ngày 31/10/2026):

```
You are a helpful assistant inside a to-do app. You can read and change the user's to-dos only through the provided tools.
Today is ${today.toSpokenDate()}.

Rules:
- The user's to-dos are their tasks and plans. For any question about what they need to do, have done or plan to do, call a tool first. Never answer from memory and never make up to-dos.
- To find to-dos about a topic, like "Do I need to record something?", call search_todos with a keyword. To find to-dos by due date, call get_todos.
- Never calculate dates yourself. Pass dates and periods to the tools in the user's own words, like "friday" or "in 3 weeks". The app works out the exact date.
- If a tool can't understand a date, try again with simpler wording or ask the user.
- Keep titles short and don't put the due date into the title.
- Refer to to-dos by their title.
- Keep answers short and write plain text without Markdown, since the chat can't render it.
- Use the dates exactly as the tools return them.
- When listing to-dos, put each one on its own line starting with "• ".
```

Theo Philipp (video đăng ngày 25/10/2026), đây bình thường là việc mà các LLM hiện đại làm rất, rất tốt. Nhưng đó là một điểm lạ bạn sẽ gặp, riêng với các agent on-device yếu hơn này. Và tóm gọn, theo anh, mọi thứ hoạt động như vậy.

## 15. Tải model bằng một HTTP request bình thường

Philipp đã hứa cho xem phần tải model, và đây là nó (video đăng ngày 25/10/2026): chỉ là một hàm dùng một HTTP client của Ktor và một hàm download. Nó chỉ tải model về từ một URL thuần, trả về tiến độ tải, và về cơ bản chỉ gửi một request tới URL đó.

![File ModelDownloader.kt: hàm download(url, targetFile, expectedSizeBytes) trả về Flow, dùng file .part và header Range](https://homus.dev/photos/fW1wKSBDBHA-2423.jpg)

Class `ModelDownloader` trên màn hình (video đăng ngày 25/10/2026): hàm `download` trả về `Flow<DownloadProgress>`; comment ghi rằng dữ liệu được ghi vào một file `.part` trước, để một lần tải bị ngắt sẽ tiếp tục từ chỗ dừng ở lần gọi sau, nhờ header `Range` với số byte đã có.

Theo Philipp (video đăng ngày 25/10/2026), tải một file trong phần lớn trường hợp không khác gì gửi một REST request bình thường, chỉ có điều bản thân request sẽ mất nhiều thời gian hơn. Code chạy lần tải này, rồi chỉ việc emit tiến độ, để app hiện một thanh tiến độ khi lần tải đầu tiên bắt đầu.

Philipp cảm ơn mọi người đã xem, chúc mọi người một phần còn lại của tuần thật tuyệt, và chào tạm biệt.

## Nguồn và liên kết

- Tên gốc: Beginner's Guide to Integrating On-Device AI Agents In Your Mobile App (Android + iOS)
- Sự kiện: [YouTube](https://homus.dev/events/youtube)
- Video gốc: [https://www.youtube.com/watch?v=fW1wKSBDBHA](https://www.youtube.com/watch?v=fW1wKSBDBHA)

- Kênh YouTube Philipp Lackner (Philipp Lackner): [youtube.com/channel/UCKNTZMRHPLXfqlbdOI7mCkg](https://www.youtube.com/channel/UCKNTZMRHPLXfqlbdOI7mCkg)

- Repo TodoAI, app to-do Kotlin Multiplatform dùng trong video (file `TodoAgent.kt` chứa system prompt và `MAX_TOOL_ROUNDS = 5`, `LocalModel.kt` ghi model Gemma 4 E2B; đọc ngày 31/10/2026): [github.com/philipplackner/TodoAI](https://github.com/philipplackner/TodoAI)

- LiteRT-LM, framework open source của Google để chạy LLM trên thiết bị edge (đọc ngày 31/10/2026): [github.com/google-ai-edge/LiteRT-LM](https://github.com/google-ai-edge/LiteRT-LM)

- Gemma 4 model overview trên Google AI for Developers (đọc ngày 31/10/2026): [ai.google.dev/gemma/docs/core](https://ai.google.dev/gemma/docs/core)

- Collection Gemma 4 của Google trên Hugging Face (đọc ngày 31/10/2026): [huggingface.co/collections/google/gemma-4](https://huggingface.co/collections/google/gemma-4)

- Firebase AI Logic, "Build hybrid and on-device experiences" (đọc ngày 31/10/2026): [firebase.google.com/docs/ai-logic/hybrid](https://firebase.google.com/docs/ai-logic/hybrid)

- Gemini Nano trên Android Developers (đọc ngày 31/10/2026): [developer.android.com/ai/gemini-nano](https://developer.android.com/ai/gemini-nano)

- Foundation Models trên Apple Developer Documentation (đọc ngày 31/10/2026): [developer.apple.com/documentation/foundationmodels](https://developer.apple.com/documentation/foundationmodels)

Nguồn: https://homus.dev/talks/beginners-guide-to-integrating-on-device-ai-agents-in-your-mobile-app-android-ios · cập nhật 2026-10-31
