✨ Agno: a company of AI agents, managed from Python · Agno: บริษัทของเอเจนต์ AI ที่บริหารด้วย Python · Agno:用 Python 管理的一家 AI 智能体公司

How to run a small team of AI agents with Agno, a Python library, and nothing else: no second program to install, no account, no key. In ten short files you hire a cashier and a cook, give them a boss, keep their conversations, make one of them wait for your yes, give them a budget, and serve the whole shop to other programs. A last file turns three agents into a research paper assistant. Everything runs on your own computer, and every output on this page came from a real run. · วิธีดูแลทีมเอเจนต์ AI ขนาดเล็กด้วย Agno ซึ่งเป็นไลบรารีของ Python โดยไม่ต้องใช้อย่างอื่น: ไม่ต้องติดตั้งโปรแกรมที่สอง ไม่ต้องมีบัญชี ไม่ต้องมีคีย์ ด้วยไฟล์สั้น ๆ สิบไฟล์ คุณจะจ้างแคชเชียร์กับพ่อครัว หาหัวหน้าให้พวกเขา เก็บบทสนทนาของพวกเขา ให้ตัวหนึ่งรอคำว่า “ได้” จากคุณ ตั้งงบประมาณให้ และเปิดทั้งร้านให้โปรแกรมอื่นเข้ามาใช้ ไฟล์สุดท้ายเปลี่ยนเอเจนต์สามตัวให้เป็นผู้ช่วยทำรายงานวิจัย ทุกอย่างรันบนคอมพิวเตอร์ของคุณเอง และผลลัพธ์ทุกชิ้นในหน้านี้มาจากการรันจริง · 怎样只用 Agno 这个 Python 库来管理一小队 AI 智能体:不用再装第二个程序,不用账号,不用密钥。用十个很短的文件,你会雇一个收银员和一个厨师,给他们一个老板,保存他们的对话,让其中一个等你点头,给他们定预算,再把整家店开放给别的程序使用。最后一个文件把三个智能体变成一个研究报告助手。一切都运行在你自己的电脑上,这一页的每一段输出都来自真实的运行。

🇬🇧 English

One AI agent is easy to watch. A team of them raises questions that have nothing to do with AI. Who has which job? What did each one say and do? May this one give money back without asking? What has it all cost? Paperclip with Python answered those questions with Paperclip, a separate program written in another language. This page answers them with a Python library, so the manager is a file you wrote and can read.

The company is the same one: Three Geckos Noodles, a noodle shop in Chiang Mai. You will write ten short files. One agent that answers a question. A cashier that adds up a bill with a tool. The whole shop as one Python file. A conversation that is saved. A boss who gives each question to the right employee. A refund that waits for the owner's yes. A budget. A report read from the books. And last, the shop as a small server that any program can talk to.

You need Python 3.10 or newer, and Ollama with two free models, of 1.9 GB and 9.0 GB. You need no account, no key and no money. Nothing you type leaves your computer until the last tutorial, whose researcher sends its searches to the web. You type each command and compare what you get with what we got.

Nothing here is copied from a manual in the hope that it works. Every file was run on 10 October 2026 on a laptop with no graphics card, and each box of output is the saved output of that run. Three things went wrong for us on the way, and the page keeps all three, with the check that shows each and the line that fixed it.

🇹🇭 ไทย

เอเจนต์ AI ตัวเดียวตามดูได้ง่าย แต่เมื่อเป็นทีม ก็มีคำถามที่ไม่เกี่ยวกับ AI เลย ใครทำงานอะไร แต่ละตัวพูดอะไรและทำอะไรไป ตัวนี้คืนเงินได้เองโดยไม่ต้องถามหรือไม่ ทั้งหมดใช้ไปเท่าไร หน้า Paperclip ด้วย Python ตอบคำถามเหล่านี้ด้วย Paperclip ซึ่งเป็นโปรแกรมแยกต่างหากที่เขียนด้วยอีกภาษาหนึ่ง หน้านี้ตอบด้วยไลบรารีของ Python ผู้จัดการจึงเป็นไฟล์ที่คุณเขียนเองและอ่านได้

บริษัทยังเป็นแห่งเดิม: Three Geckos Noodles ร้านก๋วยเตี๋ยวในเชียงใหม่ คุณจะเขียนไฟล์สั้น ๆ สิบไฟล์ เอเจนต์หนึ่งตัวที่ตอบคำถาม แคชเชียร์ที่คิดบิลด้วยเครื่องมือ ทั้งร้านในไฟล์ Python ไฟล์เดียว บทสนทนาที่ถูกบันทึกไว้ หัวหน้าที่ส่งแต่ละคำถามให้พนักงานที่ถูกคน การคืนเงินที่รอคำว่า “ได้” จากเจ้าของ งบประมาณ รายงานที่อ่านจากสมุดบัญชี และสุดท้าย ร้านในรูปเซิร์ฟเวอร์เล็ก ๆ ที่โปรแกรมใดก็คุยด้วยได้

คุณต้องมี Python 3.10 ขึ้นไป และ Ollama พร้อมโมเดลฟรีสองตัว ขนาด 1.9 GB และ 9.0 GB ไม่ต้องมีบัญชี ไม่ต้องมีคีย์ และไม่ต้องเสียเงิน ไม่มีอะไรที่คุณพิมพ์ออกไปนอกคอมพิวเตอร์ของคุณ จนถึงบทสุดท้าย ซึ่งนักค้นคว้าส่งคำค้นออกไปยังเว็บ คุณพิมพ์แต่ละคำสั่งแล้วเทียบสิ่งที่ได้กับสิ่งที่เราได้

ไม่มีอะไรในหน้านี้ที่ลอกมาจากคู่มือแล้วหวังว่าจะใช้ได้ ทุกไฟล์รันเมื่อวันที่ 10 ตุลาคม 2026 บนแล็ปท็อปที่ไม่มีการ์ดจอ และกล่องผลลัพธ์แต่ละกล่องคือผลลัพธ์ที่บันทึกไว้จากการรันครั้งนั้น ระหว่างทางมีสามเรื่องที่ผิดพลาดสำหรับเรา หน้านี้เก็บไว้ทั้งสามเรื่อง พร้อมสคริปต์ตรวจสอบที่แสดงแต่ละเรื่อง และบรรทัดที่แก้มันได้

🇨🇳 中文

一个 AI 智能体很容易盯住。换成一个团队,就冒出一些和 AI 毫无关系的问题。谁负责哪件事?每一个说了什么、做了什么?这一个能不能不问一声就退钱?这一切总共花了多少?《用 Python 驱动 Paperclip》用 Paperclip 来回答这些问题,那是另一种语言写的一个独立程序。这一页用一个 Python 库来回答,所以管理者就是你自己写的、读得懂的一个文件。

公司还是那一家:Three Geckos Noodles,清迈的一家面馆。你会写十个很短的文件。一个回答问题的智能体。一个用工具算账的收银员。写在一个 Python 文件里的整家店。一段被保存下来的对话。一个把每个问题交给合适员工的老板。一笔要等老板点头的退款。一份预算。一份从账本里读出来的报告。最后,是把这家店做成一个小服务器,任何程序都能和它对话。

你需要 Python 3.10 或更新的版本,以及 Ollama 和两个免费模型,分别是 1.9 GB 和 9.0 GB。不需要账号,不需要密钥,也不用花钱。在最后一个教程之前,你输入的任何东西都不会离开你的电脑;到了那里,研究员会把搜索词发到网上。每条命令你都自己输入,再把得到的结果和我们的对比。

这里没有哪一步是从手册上抄来、指望它能用的。每个文件都在 2026 年 10 月 10 日、在一台没有显卡的笔记本上运行过,每个输出框都是那次运行保存下来的输出。一路上我们遇到了三次出错,这一页把三次都留着,连同展示每一次的检查脚本,以及把它修好的那一行。

THE MODEL READS. PYTHON COUNTS. One idea runs through the whole page. A language model is good at reading a messy sentence and choosing what to do about it. It is not to be trusted with a sum, a price or a rule. So in every file the model does the reading and the choosing, and an ordinary Python function does the part that must be right. The manager's job is to keep those two apart, and to write down which did what.มีแนวคิดเดียวที่ร้อยทั้งหน้านี้ไว้ โมเดลภาษาเก่งเรื่องอ่านประโยคที่ไม่เป็นระเบียบแล้วเลือกว่าจะทำอะไรกับมัน แต่ไม่ควรไว้ใจให้มันบวกเลข บอกราคา หรือรักษากฎ ดังนั้นในทุกไฟล์ โมเดลเป็นฝ่ายอ่านและเลือก ส่วนฟังก์ชัน Python ธรรมดาทำส่วนที่ต้องถูกต้อง หน้าที่ของผู้จัดการคือแยกสองอย่างนี้ออกจากกัน และจดไว้ว่าฝ่ายไหนทำอะไร有一个想法贯穿整页。语言模型擅长读一句乱糟糟的话,再决定该拿它怎么办。但算账、报价、守规矩,这些事不能托付给它。所以在每个文件里,读和选都交给模型,必须正确的那部分交给一个普通的 Python 函数。管理者的工作,就是把这两者分开,并记下哪一方做了什么。
Fileไฟล์文件What it isคืออะไร它是什么Stepขั้นที่步骤
hello.pyOne agent, one question, one answer.เอเจนต์หนึ่งตัว คำถามหนึ่งข้อ คำตอบหนึ่งข้อ一个智能体,一个问题,一个回答。2
cashier.pyAn agent with one tool: a Python function that adds up a bill.เอเจนต์ที่มีเครื่องมือหนึ่งอย่าง: ฟังก์ชัน Python ที่คิดบิล一个带一件工具的智能体:一个算账单的 Python 函数。3
shop.pyThe company: a cashier, a cook, a refunds desk, their boss and the books. The files after it import from it.ตัวบริษัท: แคชเชียร์ พ่อครัว โต๊ะคืนเงิน หัวหน้าของพวกเขา และสมุดบัญชี ไฟล์ที่ตามมา import จากไฟล์นี้公司本身:收银员、厨师、退款台、他们的老板,还有账本。后面的文件都从它这里 import。4
order.pyThree messages from one table, in one saved conversation.ข้อความสามข้อความจากโต๊ะเดียว ในบทสนทนาเดียวที่ถูกบันทึกไว้同一桌发来的三条消息,在同一段被保存的对话里。5
team.pyAsk the shop, and see which employee the boss chose.ถามร้าน แล้วดูว่าหัวหน้าเลือกพนักงานคนไหน向店里提问,看老板选了哪个员工。6
approve.pyA refund that waits for the owner's yes or no.การคืนเงินที่รอคำตอบว่าได้หรือไม่ได้จากเจ้าของ一笔等老板说行或不行的退款。7
budget.pyA budget of tokens that stops the cashier.งบประมาณโทเคนที่หยุดแคชเชียร์ได้一份能让收银员停下来的 token 预算。8
runs.pyReads the books: every conversation, run and tool call.อ่านสมุดบัญชี: ทุกบทสนทนา ทุกการรัน และทุกการเรียกเครื่องมือ读账本:每一段对话、每一次运行、每一次工具调用。9
serve.py, client.pyThe shop as a server, and a program that talks to it.ร้านในรูปเซิร์ฟเวอร์ และโปรแกรมที่คุยกับมัน做成服务器的店,以及一个和它对话的程序。10
research.pyA research paper assistant: a reviewer, a researcher, a writer and a loop.ผู้ช่วยทำรายงานวิจัย: ผู้รีวิว นักค้นคว้า นักเขียน และลูปหนึ่งลูป研究报告助手:审稿人、研究员、写手,加一个循环。11
Wordคำ词What it means on this pageความหมายในหน้านี้在这一页里的意思
modelThe AI program that reads text and writes text. Here it runs on your computer, in Ollama.โมเดล: โปรแกรม AI ที่อ่านข้อความและเขียนข้อความ ในหน้านี้มันรันบนคอมพิวเตอร์ของคุณ ใน Ollama模型:读文字、写文字的 AI 程序。在这里,它运行在你的电脑上,在 Ollama 里。
agentA model with a job: instructions, and usually tools.เอเจนต์: โมเดลที่มีงานทำ คือมีคำสั่ง และมักมีเครื่องมือ智能体:有了一份工作的模型,带着指令,通常还带着工具。
toolA Python function that an agent may ask to have run.เครื่องมือ: ฟังก์ชัน Python ที่เอเจนต์ขอให้รันได้工具:智能体可以要求运行的一个 Python 函数。
runOne message to an agent, with everything that happened until its answer.การรัน: ข้อความหนึ่งข้อความถึงเอเจนต์ พร้อมทุกอย่างที่เกิดขึ้นจนได้คำตอบ运行:发给智能体的一条消息,加上直到它回答为止发生的一切。
sessionOne conversation: the runs that belong together, under one name.เซสชัน: บทสนทนาหนึ่งบท คือการรันที่อยู่ด้วยกัน ภายใต้ชื่อเดียว会话:一段对话,也就是归在同一个名字下的那些运行。
teamSeveral agents and a leader that decides who works on a message.ทีม: เอเจนต์หลายตัวกับหัวหน้าที่ตัดสินว่าใครจะทำงานกับข้อความหนึ่ง团队:几个智能体,加上一个决定由谁来处理消息的领导。
tokenA piece of a word. Models count their reading and writing in tokens, and paid models bill by them.โทเคน: ชิ้นส่วนของคำ โมเดลนับการอ่านและการเขียนเป็นโทเคน และโมเดลเสียเงินคิดเงินตามโทเคนtoken:词的一小片。模型用 token 来计算读了多少、写了多少,收费的模型也按它计费。
hookA function of yours that the library calls at a fixed moment, such as before every run.hook: ฟังก์ชันของคุณที่ไลบรารีเรียกในจังหวะที่กำหนด เช่นก่อนการรันทุกครั้งhook:你写的一个函数,由库在固定的时刻调用,比如每次运行之前。

🔭 Why Agno · ทำไมต้อง Agno · 为什么选 Agno

🇬🇧 English

There are many Python libraries for teams of agents, and “the best” depends on the job. We asked GitHub and the Python Package Index about eight well-known ones on the day we wrote this. A star means that one person marked the project. It measures attention, not quality.

🇹🇭 ไทย

มีไลบรารี Python สำหรับทีมเอเจนต์อยู่มากมาย และคำว่า “ดีที่สุด” ขึ้นกับงาน ในวันที่เขียนหน้านี้ เราถาม GitHub และ Python Package Index เกี่ยวกับไลบรารีที่เป็นที่รู้จักแปดตัว ดาวหนึ่งดวงหมายความว่ามีคนหนึ่งคนกดให้โปรเจกต์นั้น มันวัดความสนใจ ไม่ได้วัดคุณภาพ

🇨🇳 中文

给智能体团队用的 Python 库有很多,“最好”取决于要做的事。写这一页的当天,我们向 GitHub 和 Python Package Index 查了八个知名的库。一颗星表示有一个人给这个项目点了标记。它衡量的是关注度,不是质量。

Asked on 2026-10-10
project                          stars  licence      last change  newest version
microsoft/autogen               61,332  CC-BY-4.0    2026-04-15   0.7.5
crewAIInc/crewAI                59,511  MIT          2026-10-09   1.15.27
langchain-ai/langgraph          42,979  MIT          2026-10-10   1.2.14
agno-agi/agno                   42,640  Apache-2.0   2026-10-10   3.1.2
openai/openai-agents-python     29,937  MIT          2026-10-08   0.23.1
huggingface/smolagents          29,757  Apache-2.0   2026-10-06   1.26.0
pydantic/pydantic-ai            20,514  MIT          2026-10-10   2.55.0
microsoft/agent-framework       14,041  MIT          2026-10-09   1.21.0

🇬🇧 English

The job here is managing: who works, who waits, what is kept, what it costs, and a way for other programs to reach the team. For that job we chose Agno, and these are our reasons. They are a judgment, and you may weigh them differently.

First, the manager comes in the box. Agno's documentation describes three layers: an SDK with which you build agents, teams, and workflows, a server it calls AgentOS that will run your agent platform as a secure and durable API and MCP server, and a control plane on top. The first two are in the one library you install, which is why the server in step 10 is under thirty lines.

Second, it is free software in Python, under the Apache 2.0 licence, changed on the day we looked, and among the four most starred of the eight. Third, it works with a model on your own computer. Fourth, it is the one this site had no page for: CrewAI and AutoGen, LangGraph and smolagents each have theirs, and they are good choices for other jobs.

🇹🇭 ไทย

งานของหน้านี้คือการจัดการ: ใครทำงาน ใครรอ เก็บอะไรไว้ ใช้ไปเท่าไร และทางที่โปรแกรมอื่นจะเข้าถึงทีมได้ สำหรับงานนี้เราเลือก Agno และนี่คือเหตุผลของเรา มันเป็นการตัดสินใจของเรา คุณอาจให้น้ำหนักต่างออกไปได้

ข้อแรก ผู้จัดการมากับกล่องเลย เอกสารของ Agno อธิบายว่ามีสามชั้น: SDK ที่ใช้ build agents, teams, and workflows เซิร์ฟเวอร์ที่เรียกว่า AgentOS ซึ่งจะ run your agent platform as a secure and durable API and MCP server และ control plane ที่อยู่ข้างบน สองอย่างแรกอยู่ในไลบรารีเดียวที่คุณติดตั้ง นี่คือเหตุผลที่เซิร์ฟเวอร์ในขั้นที่ 10 ยาวไม่ถึงสามสิบบรรทัด

ข้อสอง มันเป็นซอฟต์แวร์เสรีที่เขียนด้วย Python ภายใต้สัญญาอนุญาต Apache 2.0 มีการแก้ไขในวันที่เราดู และอยู่ในสี่อันดับแรกที่มีดาวมากที่สุดจากแปดตัว ข้อสาม มันใช้กับโมเดลบนคอมพิวเตอร์ของคุณเองได้ ข้อสี่ มันเป็นตัวที่เว็บไซต์นี้ยังไม่มีหน้าให้: CrewAI และ AutoGen, LangGraph และ smolagents ต่างมีหน้าของตัวเองแล้ว และเป็นตัวเลือกที่ดีสำหรับงานอื่น

🇨🇳 中文

这一页要做的事是管理:谁干活,谁等着,留下什么,花了多少,以及别的程序怎样找到这个团队。为了这件事我们选了 Agno,下面是我们的理由。这是一种判断,你完全可以有不同的权衡。

第一,管理者是随盒附带的。Agno 的文档说它有三层:一个用来 build agents, teams, and workflows 的 SDK;一个叫 AgentOS 的服务器,它会 run your agent platform as a secure and durable API and MCP server;以及上面的一层 control plane。前两样都在你安装的那一个库里,所以第 10 步的服务器不到三十行。

第二,它是用 Python 写的自由软件,采用 Apache 2.0 许可证,我们查看的当天还有改动,在八个里星数排进前四。第三,它能配合你自己电脑上的模型使用。第四,本站唯独还没有它的页面:CrewAI 和 AutoGen、LangGraph 和 smolagents 各有各的页面,做别的事时它们都是好选择。

🌱 Tutorial 1 — Install it and say hello · บทที่ 1 — ติดตั้งแล้วทักทาย · 教程 1 —— 安装,然后打个招呼

1 Install Agno and two models · ติดตั้ง Agno และโมเดลสองตัว · 安装 Agno 和两个模型

🇬🇧 English

Make a folder, and give the page a Python of its own in it. The first command makes a virtual environment in a folder named venv, and the second installs the libraries into it. On a Mac or Linux, write venv/bin/python where this page writes venv\Scripts\python.

The models come from Ollama, a free program that runs AI models on your own computer. Install it, then fetch the two models. The small one is for the employees. The big one is for the boss, and step 6 shows why the boss needs it.

🇹🇭 ไทย

สร้างโฟลเดอร์ แล้วให้หน้านี้มี Python ของตัวเองในนั้น คำสั่งแรกสร้างสภาพแวดล้อมเสมือนในโฟลเดอร์ชื่อ venv และคำสั่งที่สองติดตั้งไลบรารีลงไป บนแมคหรือ Linux ให้เขียน venv/bin/python ตรงที่หน้านี้เขียน venv\Scripts\python

โมเดลมาจาก Ollama โปรแกรมฟรีที่รันโมเดล AI บนคอมพิวเตอร์ของคุณเอง ติดตั้งมัน แล้วดึงโมเดลสองตัวมา ตัวเล็กสำหรับพนักงาน ตัวใหญ่สำหรับหัวหน้า และขั้นที่ 6 จะแสดงว่าทำไมหัวหน้าต้องใช้ตัวใหญ่

🇨🇳 中文

建一个文件夹,在里面给这一页单独准备一份 Python。第一条命令在名叫 venv 的文件夹里建一个虚拟环境,第二条把库装进去。在 Mac 或 Linux 上,凡是这一页写 venv\Scripts\python 的地方,请写 venv/bin/python。

模型来自 Ollama,一个在你自己电脑上运行 AI 模型的免费程序。装好它,再把两个模型取下来。小的给员工用。大的给老板用,第 6 步会说明老板为什么需要它。

python -m venv venv
venv\Scripts\python -m pip install agno ollama openai "sqlalchemy[asyncio]" aiosqlite "fastapi[standard]" requests
ollama pull qwen2.5:3b
ollama pull qwen2.5:14b
WHY THE LIST IS LONGER THAN “AGNO” Agno installs only its own core, and asks for each other library when you first use the part that needs it. Two of those surprised us. Its Ollama model would not import until the openai library was installed, although no file here talks to OpenAI. And its SQLite database would not import until SQLAlchemy had its asyncio extras, which is what "sqlalchemy[asyncio]" and aiosqlite are. Each time the error named the missing library, so read the last line of an error before anything else.Agno ติดตั้งเฉพาะแกนของตัวเอง และจะขอไลบรารีอื่นทีละตัวเมื่อคุณใช้ส่วนที่ต้องการมันเป็นครั้งแรก มีสองตัวที่ทำให้เราแปลกใจ โมเดล Ollama ของมัน import ไม่ได้จนกว่าจะติดตั้งไลบรารี openai ทั้งที่ไม่มีไฟล์ใดในหน้านี้คุยกับ OpenAI และฐานข้อมูล SQLite ของมัน import ไม่ได้จนกว่า SQLAlchemy จะมีส่วนเสริม asyncio ซึ่งก็คือ "sqlalchemy[asyncio]" และ aiosqlite ทุกครั้งข้อผิดพลาดจะบอกชื่อไลบรารีที่ขาด ดังนั้นให้อ่านบรรทัดสุดท้ายของข้อผิดพลาดก่อนอย่างอื่นAgno 只安装它自己的核心部分,别的库要等你第一次用到相关功能时,它才开口要。有两个让我们没想到。它的 Ollama 模型,要装了 openai 库才 import 得了,尽管这里没有一个文件和 OpenAI 打交道。它的 SQLite 数据库,要 SQLAlchemy 带上 asyncio 的附加部分才 import 得了,也就是 "sqlalchemy[asyncio]" 和 aiosqlite。每一次,报错都点出了缺的是哪个库,所以遇到错误,先读它的最后一行。

2 hello.py: one agent · hello.py: เอเจนต์หนึ่งตัว · hello.py:一个智能体

🇬🇧 English

The smallest thing Agno can do is put a question to a model. An Agent is given a model and some instructions. agent.run() sends one message and comes back with a run: the answer, and a record of what it took.

🇹🇭 ไทย

สิ่งที่เล็กที่สุดที่ Agno ทำได้คือส่งคำถามให้โมเดล Agent ได้รับโมเดลหนึ่งตัวกับคำสั่งจำนวนหนึ่ง agent.run() ส่งข้อความหนึ่งข้อความ แล้วกลับมาพร้อมการรันหนึ่งครั้ง: คำตอบ และบันทึกว่าใช้อะไรไปบ้าง

🇨🇳 中文

Agno 能做的最小的一件事,就是把一个问题交给模型。Agent 得到一个模型和一些指令。agent.run() 发出一条消息,带回一次运行:回答,以及为此花了多少的记录。

"""hello.py - one agent, one question, one answer.

    python hello.py
"""
from agno.agent import Agent
from agno.models.ollama import Ollama

MODEL = "qwen2.5:3b"


def main():
    agent = Agent(model=Ollama(id=MODEL), instructions="Answer in one short sentence.")
    run = agent.run("What is the capital of Thailand?")
    print("The agent says:", run.content)
    print("Model:", run.model)
    print("Tokens:", run.metrics.input_tokens, "in,", run.metrics.output_tokens, "out")


if __name__ == "__main__":
    main()
venv\Scripts\python hello.py
The agent says: The capital of Thailand is Bangkok.
Model: qwen2.5:3b
Tokens: 26 in, 8 out
YOU GET An answer, the name of the model that gave it, and the tokens it took to read your question and write the reply. The first run is the slow one, because Ollama has to load the model first.คำตอบ ชื่อของโมเดลที่ให้คำตอบ และจำนวนโทเคนที่ใช้อ่านคำถามของคุณและเขียนคำตอบ การรันครั้งแรกจะช้าที่สุด เพราะ Ollama ต้องโหลดโมเดลก่อน一个回答,给出回答的模型的名字,以及读你的问题、写出回答所用的 token 数。第一次运行最慢,因为 Ollama 得先把模型加载进来。

🧾 Tutorial 2 — An employee with a tool · บทที่ 2 — พนักงานที่มีเครื่องมือ · 教程 2 —— 一个带工具的员工

3 cashier.py: Python does the sum · cashier.py: ให้ Python เป็นคนบวกเลข · cashier.py:让 Python 来算账

🇬🇧 English

A cashier must not guess. So the sum is an ordinary Python function, bill, with the prices in a dictionary. Handing that function to the agent in tools=[bill] makes it a tool: the model may ask for it to be run, and Agno runs it and gives the model what it returned.

🇹🇭 ไทย

แคชเชียร์ต้องไม่เดา การบวกเลขจึงเป็นฟังก์ชัน Python ธรรมดาชื่อ bill โดยมีราคาอยู่ในดิกชันนารี การส่งฟังก์ชันนั้นให้เอเจนต์ผ่าน tools=[bill] ทำให้มันกลายเป็นเครื่องมือ: โมเดลขอให้รันมันได้ แล้ว Agno จะรันและส่งสิ่งที่มันคืนกลับมาให้โมเดล

🇨🇳 中文

收银员不能靠猜。所以算账是一个普通的 Python 函数 bill,价格放在一个字典里。用 tools=[bill] 把这个函数交给智能体,它就成了一件工具:模型可以要求运行它,Agno 负责运行,再把返回的结果交给模型。

"""cashier.py - an agent with one tool. The model reads the order, Python does the sum.

    python cashier.py "2 khao soi, 1 thai iced tea"
"""
import sys

from agno.agent import Agent
from agno.models.ollama import Ollama

MODEL = "qwen2.5:3b"
PRICES = {"khao soi": 60, "nam ngiao": 50, "pad thai": 60, "pad see ew": 55,
          "thai iced tea": 30, "longan juice": 25, "water": 10}


def bill(order: str) -> str:
    """Add up an order of the noodle shop.

    Args:
        order: the dishes and how many of each, such as "2 khao soi, 1 water"
    """
    lines = []
    total = 0
    for part in order.lower().split(","):
        words = part.split()
        if len(words) < 2 or not words[0].isdigit():
            return "I cannot read this part of the order: " + part.strip()
        count = int(words[0])
        dish = " ".join(words[1:])
        if dish not in PRICES:
            return "I do not know the price of " + dish + "."
        cost = count * PRICES[dish]
        lines.append(f"- {count} x {dish} = {cost} baht")
        total = total + cost
    lines.append(f"- Total: {total} baht")
    return "\n".join(lines)


def main():
    order = sys.argv[1]
    cashier = Agent(
        name="Cashier",
        # temperature 0: the model gives the likeliest answer every time
        model=Ollama(id=MODEL, options={"temperature": 0}),
        tools=[bill],
        instructions="You are the cashier of a noodle shop. Use the bill tool for "
                     "every order. Then reply with the bill the tool returned, word "
                     "for word. Never work out or guess a price yourself.",
    )
    run = cashier.run(order)
    for call in run.tools:
        print("The agent called:", call.tool_name, call.tool_args)
        print("The tool returned:")
        print(call.result)
    print("The agent says:")
    print(run.content)


if __name__ == "__main__":
    main()
venv\Scripts\python cashier.py "2 khao soi, 1 thai iced tea"
The agent called: bill {'order': '2 khao soi, 1 thai iced tea'}
The tool returned:
- 2 x khao soi = 120 baht
- 1 x thai iced tea = 30 baht
- Total: 150 baht
The agent says:
The total bill is 150 baht.

🇬🇧 English

Read the output from the top. The model did not add anything up. It read the order and asked for bill, with the order as the argument. Python returned three exact lines. Then the model wrote one sentence of its own about them.

How does the model know what bill is for? It never sees the code. Agno shows it the function's name, its docstring, and its argument with its type. So the docstring is not a note for you: it is the instruction the model reads. The type matters just as much. We ran the same order through two copies of the tool, one with order: str and one with plain order.

🇹🇭 ไทย

อ่านผลลัพธ์จากบนลงล่าง โมเดลไม่ได้บวกอะไรเลย มันอ่านรายการสั่งแล้วขอใช้ bill โดยส่งรายการสั่งเป็นอาร์กิวเมนต์ Python คืนสามบรรทัดที่ถูกต้องแน่นอน จากนั้นโมเดลเขียนประโยคของตัวเองหนึ่งประโยคเกี่ยวกับมัน

โมเดลรู้ได้อย่างไรว่า bill ใช้ทำอะไร มันไม่เคยเห็นโค้ด Agno แสดงให้มันเห็นชื่อฟังก์ชัน docstring และอาร์กิวเมนต์พร้อมชนิดข้อมูล ดังนั้น docstring ไม่ใช่โน้ตสำหรับคุณ มันคือคำสั่งที่โมเดลอ่าน ชนิดข้อมูลก็สำคัญไม่แพ้กัน เราส่งรายการสั่งเดียวกันผ่านเครื่องมือสองชุด ชุดหนึ่งเขียน order: str อีกชุดเขียนแค่ order

🇨🇳 中文

从上往下读这段输出。模型什么也没有加。它读了订单,然后要求调用 bill,把订单作为参数。Python 返回了分毫不差的三行。接着模型就这三行写了一句自己的话。

模型怎么知道 bill 是干什么的?它从来看不到代码。Agno 给它看的是函数的名字、docstring,以及带类型的参数。所以 docstring 不是写给你的备注,而是模型读到的说明。类型同样要紧。我们让同一份订单经过工具的两个副本,一个写的是 order: str,一个只写 order。

bill_with_hint:
    I was handed a str: '2 khao soi, 1 thai iced tea'
bill_without_hint:
    I was handed a dict: {'khao soi': 2, 'thai iced tea': 1}
THE TYPE HINT IS PART OF THE TOOL Without : str the model was not told that the order is a piece of text, and it sent a dictionary. The function bill starts with order.lower(), and a dictionary has no such method, so the real tool would have stopped with an error. Write the type of every argument of a tool.เมื่อไม่มี : str โมเดลไม่ได้รับการบอกว่ารายการสั่งเป็นข้อความ มันจึงส่งดิกชันนารีมา ฟังก์ชัน bill เริ่มด้วย order.lower() และดิกชันนารีไม่มีเมท็อดนั้น เครื่องมือตัวจริงจึงจะหยุดด้วยข้อผิดพลาด ให้เขียนชนิดข้อมูลของอาร์กิวเมนต์ทุกตัวของเครื่องมือ没有 : str,就没人告诉模型订单是一段文字,于是它送来了一个字典。函数 bill 一开头就是 order.lower(),而字典没有这个方法,所以真正的工具会报错停下。工具的每一个参数,都要写上类型。

🏪 Tutorial 3 — A company in one file · บทที่ 3 — บริษัทในไฟล์เดียว · 教程 3 —— 一个文件里的公司

4 shop.py: the org chart · shop.py: ผังองค์กร · shop.py:组织架构

🇬🇧 English

From here on the files share one company, so the company gets a file of its own, and the others import from it. That import is the point of this step: with Agno, a company of agents is a Python module. Nothing is built when the file is imported. Each function builds its part when it is called.

It holds three employees, each with one tool and each given the same file to keep records in, which this page calls the books. And it holds the team: Noi, the owner, who has no tool, only two employees to choose between.

🇹🇭 ไทย

จากตรงนี้ไป ไฟล์ทั้งหมดใช้บริษัทเดียวกัน บริษัทจึงมีไฟล์ของตัวเอง และไฟล์อื่น import จากมัน การ import นี้คือประเด็นของขั้นนี้: ใน Agno บริษัทของเอเจนต์คือโมดูล Python หนึ่งโมดูล ไม่มีอะไรถูกสร้างตอนที่ไฟล์ถูก import แต่ละฟังก์ชันสร้างส่วนของตัวเองเมื่อถูกเรียก

ในไฟล์มีพนักงานสามคน แต่ละคนมีเครื่องมือหนึ่งอย่าง และแต่ละคนได้รับไฟล์เดียวกันไว้เก็บบันทึก ซึ่งหน้านี้เรียกว่าสมุดบัญชี และมีทีม: น้อย เจ้าของร้าน ซึ่งไม่มีเครื่องมือ มีเพียงพนักงานสองคนให้เลือก

🇨🇳 中文

从这里开始,各个文件共用一家公司,所以公司有了自己的文件,别的文件都从它这里 import。这次 import 正是这一步要讲的:在 Agno 里,一家由智能体组成的公司就是一个 Python 模块。文件被 import 时什么也不会建出来。每个函数在被调用时才建出它负责的那一部分。

里面有三个员工,各有一件工具,并且都拿到同一个用来存记录的文件,这一页把它叫作账本。还有团队:老板 Noi,她没有工具,只有两个员工可选。

Show the whole file · แสดงทั้งไฟล์ · 显示整个文件
"""shop.py - the whole company in one file: two employees, their boss, and the books.

    python shop.py          prints the org chart, and starts no model

The other files on the page import from this one. Nothing is built when it is
imported: each function builds its part when it is called.
"""
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.ollama import Ollama
from agno.team import Team
from agno.team.mode import TeamMode
from agno.tools import tool

WORKER = "qwen2.5:3b"       # the model the employees think with
LEADER = "qwen2.5:14b"      # the model the boss thinks with: bigger, and slower
DB_FILE = "shop.db"         # the books: every conversation is saved in this file
PRICES = {"khao soi": 60, "nam ngiao": 50, "pad thai": 60, "pad see ew": 55,
          "thai iced tea": 30, "longan juice": 25, "water": 10}
MINUTES = {"khao soi": 6, "nam ngiao": 7, "pad thai": 8, "pad see ew": 8,
           "thai iced tea": 2, "longan juice": 1}


# stop_after_tool_call: what this function returns is the answer. The model
# chooses the tool and reads the order, and it does not get to reword the bill.
@tool(stop_after_tool_call=True)
def bill(order: str) -> str:
    """Add up an order of the noodle shop.

    Args:
        order: the dishes and how many of each, such as "2 khao soi, 1 water"
    """
    lines = []
    total = 0
    for part in order.lower().split(","):
        words = part.split()
        if len(words) < 2 or not words[0].isdigit():
            return "I cannot read this part of the order: " + part.strip()
        count = int(words[0])
        dish = " ".join(words[1:])
        if dish not in PRICES:
            return "I do not know the price of " + dish + "."
        cost = count * PRICES[dish]
        lines.append(f"- {count} x {dish} = {cost} baht")
        total = total + cost
    lines.append(f"- Total: {total} baht")
    return "\n".join(lines)


@tool(stop_after_tool_call=True)
def cooking_time(dish: str) -> str:
    """Say how many minutes one dish of the noodle shop takes to cook.

    Args:
        dish: the name of one dish, such as "pad thai"
    """
    dish = dish.lower().strip()
    if dish not in MINUTES:
        return "We do not cook " + dish + "."
    return f"{dish} takes {MINUTES[dish]} minutes."


# requires_confirmation: the agent may ask for this tool, and then it has to wait
# until a person says yes.
@tool(requires_confirmation=True)
def refund(baht: int) -> str:
    """Give money back to a customer.

    Args:
        baht: how many baht to give back
    """
    return f"Refunded {baht} baht."


def local_model(name):
    # temperature 0: the model gives its likeliest answer every time
    return Ollama(id=name, options={"temperature": 0})


def books():
    """The file where Agno saves every conversation and every run."""
    return SqliteDb(db_file=DB_FILE)


def cashier(db):
    return Agent(
        name="Cashier", id="cashier", role="Adds up the bill for an order",
        model=local_model(WORKER), tools=[bill], db=db,
        # the agent is shown what was said before in the same conversation
        add_history_to_context=True,
        instructions="You are the cashier of a noodle shop. Use the bill tool for "
                     "every order. Never work out or guess a price yourself.",
    )


def refunds(db):
    return Agent(
        name="Refunds", id="refunds", role="Gives money back, when the owner agrees",
        model=local_model(WORKER), tools=[refund], db=db,
        instructions="When you are asked to give money back, use the refund tool "
                     "with the number of baht. Then say what the tool returned.",
    )


def cook(db):
    return Agent(
        name="Cook", id="cook", role="Knows how long each dish takes to cook",
        model=local_model(WORKER), tools=[cooking_time], db=db,
        instructions="You are the cook of a noodle shop. Use the cooking_time tool "
                     "for every question about a dish.",
    )


def team(db, leader=LEADER):
    return Team(
        name="Three Geckos Noodles", id="three-geckos", model=local_model(leader),
        members=[cashier(db), cook(db)], db=db,
        # route: the boss picks one employee, and that employee's answer is the answer
        mode=TeamMode.route,
        # the employee gets the customer's own words, not the boss's version of them
        determine_input_for_members=False,
        instructions="You are Noi, the owner of a noodle shop. You never answer "
                     "yourself. An order, which is dishes with numbers, goes to the "
                     "Cashier, who writes the bill. A question about how long a dish "
                     "takes goes to the Cook.",
    )


def main():
    shop = team(books())
    print(shop.name)
    print("    Noi, the owner. Model:", shop.model.id)
    for member in shop.members:
        # a plain function has __name__; one wrapped by @tool has name
        tools = ", ".join(getattr(one, "__name__", None) or one.name for one in member.tools)
        print("        " + member.name, "(" + member.role + ")")
        print("            model:", member.model.id, "  tools:", tools)


if __name__ == "__main__":
    main()
venv\Scripts\python shop.py
Three Geckos Noodles
    Noi, the owner. Model: qwen2.5:14b
        Cashier (Adds up the bill for an order)
            model: qwen2.5:3b   tools: bill
        Cook (Knows how long each dish takes to cook)
            model: qwen2.5:3b   tools: cooking_time
In shop.pyใน shop.pyshop.py 里的What it decidesกำหนดอะไร它决定什么
SqliteDb(db_file=...)Where the records go: one file, shop.db, beside your code. Every agent that is given it writes its runs there.บันทึกไปอยู่ที่ไหน: ไฟล์เดียวชื่อ shop.db ข้างโค้ดของคุณ เอเจนต์ทุกตัวที่ได้รับมันจะเขียนการรันของตัวเองลงไป记录存到哪里:就在你的代码旁边的一个文件 shop.db。每个拿到它的智能体,都把自己的运行写在里面。
stop_after_tool_call=TrueWhat the tool returns is the answer. The model does not get to reword it. Step 5 shows why.สิ่งที่เครื่องมือคืนมาคือคำตอบ โมเดลไม่ได้เรียบเรียงใหม่ ขั้นที่ 5 แสดงเหตุผล工具返回什么,回答就是什么。模型没有机会改写。第 5 步说明原因。
requires_confirmation=TrueThe tool waits for a person. Step 7 uses it.เครื่องมือนี้รอคน ขั้นที่ 7 ใช้มัน这件工具要等人。第 7 步会用到。
add_history_to_context=TrueThe agent is shown the earlier messages of the same session.เอเจนต์จะได้เห็นข้อความก่อนหน้าในเซสชันเดียวกัน智能体能看到同一个会话里先前的消息。
mode=TeamMode.routeThe boss picks one employee, and that employee's answer is the team's answer.หัวหน้าเลือกพนักงานหนึ่งคน และคำตอบของพนักงานคนนั้นคือคำตอบของทีม老板挑一个员工,这个员工的回答就是团队的回答。
temperature 0The model gives its likeliest answer each time, so two runs on one computer agree.โมเดลให้คำตอบที่น่าจะเป็นที่สุดทุกครั้ง การรันสองครั้งบนคอมพิวเตอร์เครื่องเดียวจึงตรงกัน模型每次都给出可能性最大的回答,所以在同一台电脑上,两次运行的结果一致。

5 order.py: a conversation that is kept · order.py: บทสนทนาที่ถูกเก็บไว้ · order.py:一段被保存下来的对话

🇬🇧 English

Table 4 orders three times. The second order has a dish the shop does not sell, and the third message only makes sense if you remember the second. Every run gets the same session_id, and that one name is what ties them into a conversation.

🇹🇭 ไทย

โต๊ะ 4 สั่งสามครั้ง รายการที่สองมีอาหารที่ร้านไม่ได้ขาย และข้อความที่สามจะเข้าใจได้ก็ต่อเมื่อจำข้อความที่สองได้ การรันทุกครั้งได้ session_id เดียวกัน และชื่อเดียวนี้เองที่ผูกมันเข้าเป็นบทสนทนา

🇨🇳 中文

4 号桌点了三次。第二次点的菜里有一样店里不卖,而第三条消息只有记得第二条才看得懂。每次运行都用同一个 session_id,正是这一个名字把它们串成了一段对话。

"""order.py - three messages from one table, in one conversation that is saved.

    python order.py

The second order has a dish the shop does not sell. The third message corrects
it, and the cashier is shown what was said before.
"""
from shop import books, cashier

TABLE = "table-4"       # the name of the conversation: Agno calls it a session
MESSAGES = ["2 khao soi, 1 thai iced tea",
            "1 pad thai, 2 pizza",
            "Sorry, no pizza. Make that 1 pad thai, 2 water."]


def main():
    agent = cashier(books())
    for message in MESSAGES:
        run = agent.run(message, session_id=TABLE)
        print("Customer:", message)
        for call in run.tools or []:
            print("    the model asked for:", call.tool_name, call.tool_args)
        print("Cashier:", run.content)
        print(f"    ({run.metrics.total_tokens} tokens, {run.metrics.duration:.1f} seconds)")


if __name__ == "__main__":
    main()
venv\Scripts\python order.py
Customer: 2 khao soi, 1 thai iced tea
    the model asked for: bill {'order': '2 khao soi, 1 thai iced tea'}
Cashier: - 2 x khao soi = 120 baht
- 1 x thai iced tea = 30 baht
- Total: 150 baht
    (249 tokens, 2.7 seconds)
Customer: 1 pad thai, 2 pizza
    the model asked for: bill {'order': '1 pad thai, 2 pizza'}
Cashier: I do not know the price of pizza.
    (333 tokens, 2.8 seconds)
Customer: Sorry, no pizza. Make that 1 pad thai, 2 water.
    the model asked for: bill {'order': '1 pad thai, 2 water'}
Cashier: - 1 x pad thai = 60 baht
- 2 x water = 20 baht
- Total: 80 baht
    (404 tokens, 2.2 seconds)

🇬🇧 English

The third message does not say “pad thai, 1” and “water, 2” in the tool's form. The model worked that out from the conversation and asked for bill with '1 pad thai, 2 water'. That is the reading a model is good at. The bills themselves are Python's, word for word.

They are word for word because of one line in shop.py. Our first cashier was the one from step 3, whose model writes the reply. We gave it these same three messages.

🇹🇭 ไทย

ข้อความที่สามไม่ได้เขียนว่า “pad thai, 1” และ “water, 2” ตามรูปแบบของเครื่องมือ โมเดลคิดออกจากบทสนทนา แล้วขอใช้ bill ด้วย '1 pad thai, 2 water' นี่คือการอ่านที่โมเดลทำได้ดี ส่วนตัวบิลเป็นของ Python คำต่อคำ

มันตรงคำต่อคำเพราะบรรทัดเดียวใน shop.py แคชเชียร์ตัวแรกของเราคือตัวจากขั้นที่ 3 ซึ่งโมเดลเป็นผู้เขียนคำตอบ เราส่งสามข้อความเดียวกันนี้ให้มัน

🇨🇳 中文

第三条消息并没有按工具的格式写“pad thai, 1”和“water, 2”。模型是从对话里推出来的,然后用 '1 pad thai, 2 water' 去调用 bill。这正是模型擅长的那种阅读。账单本身则是 Python 的,一字不差。

之所以一字不差,是因为 shop.py 里的一行。我们的第一个收银员就是第 3 步那个,由模型来写回复。我们把同样的三条消息给了它。

Customer: 2 khao soi, 1 thai iced tea
    the tool returned: - Total: 150 baht
    the cashier said:  The total bill is 150 baht.
    They agree.
Customer: 1 pad thai, 2 pizza
    the tool returned: I do not know the price of pizza.
    the cashier said:  I need the price of pizza to calculate the total. Please provide the price or I can only calculate f
    THEY DO NOT AGREE.
Customer: Sorry, no pizza. Make that 1 pad thai, 2 water.
    the tool returned: - Total: 80 baht
    the cashier said:  The total bill is 80 baht.
    They agree.
2 of 3 replies agree with the tool.
THE FIRST THING THAT WENT WRONG: THE MODEL'S OWN WORDS The tool said plainly that it does not know the price of pizza. The model turned that into a request for the customer to supply the price. In an earlier run of ours the same cashier answered the pizza order with the total of the order before it. The tool was right every time, and the sentence wrapped around it was not. stop_after_tool_call=True removes the sentence: the tool's own words go to the customer. It also saves a second question to the model on every order.เครื่องมือบอกชัดเจนว่าไม่รู้ราคาของพิซซ่า โมเดลกลับเปลี่ยนมันเป็นการขอให้ลูกค้าบอกราคา ในการรันครั้งก่อนหน้าของเรา แคชเชียร์ตัวเดียวกันตอบรายการพิซซ่าด้วยยอดรวมของรายการก่อนหน้า เครื่องมือถูกทุกครั้ง แต่ประโยคที่ห่อมันไว้ไม่ถูก stop_after_tool_call=True ตัดประโยคนั้นทิ้ง: คำของเครื่องมือเองไปถึงลูกค้า และยังประหยัดการถามโมเดลรอบที่สองในทุกรายการสั่งด้วย工具说得很清楚:它不知道披萨的价格。模型却把这句话变成了请顾客自己报价。在我们更早的一次运行里,同一个收银员对披萨订单的回答,竟是前一份订单的总价。工具每次都是对的,包在它外面的那句话却不是。stop_after_tool_call=True 去掉了那句话:送到顾客面前的是工具自己的原话。每份订单还因此少问模型一次。

👩‍🍳 Tutorial 4 — A boss who hands out the work · บทที่ 4 — หัวหน้าที่แจกงาน · 教程 4 —— 一个分派工作的老板

6 team.py: who gets the question · team.py: ใครได้รับคำถาม · team.py:问题交给了谁

🇬🇧 English

A customer does not know who does what in the shop. A Team has a leader with a model of its own, and that model's only work is to read the message and choose a member. In shop.py the leader is Noi, and her instructions say what goes to whom.

🇹🇭 ไทย

ลูกค้าไม่รู้ว่าใครทำอะไรในร้าน Team มีหัวหน้าที่มีโมเดลของตัวเอง และงานเดียวของโมเดลนั้นคืออ่านข้อความแล้วเลือกสมาชิก ใน shop.py หัวหน้าคือน้อย และคำสั่งของเธอบอกว่าอะไรไปหาใคร

🇨🇳 中文

顾客并不知道店里谁管什么。Team 有一个领导,领导有自己的模型,这个模型唯一的工作就是读消息、选成员。在 shop.py 里,领导是 Noi,她的指令写明了什么该交给谁。

"""team.py - ask the shop, and see which employee the boss gave the question to.

    python team.py "2 khao soi, 1 water" "How long does pad thai take?"
"""
import sys

from shop import books, team


def main():
    shop = team(books())
    for question in sys.argv[1:]:
        run = shop.run(question)
        print("Customer:", question)
        for answer in run.member_responses:
            print("    Noi gave it to:", answer.agent_name)
        print("The shop says:", run.content)
        print(f"    ({run.metrics.total_tokens} tokens, {run.metrics.duration:.1f} seconds)")


if __name__ == "__main__":
    main()
venv\Scripts\python team.py "2 khao soi, 1 water" "How long does pad thai take?"
Customer: 2 khao soi, 1 water
    Noi gave it to: Cashier
The shop says: - 2 x khao soi = 120 baht
- 1 x water = 10 baht
- Total: 130 baht
    (650 tokens, 72.6 seconds)
Customer: How long does pad thai take?
    Noi gave it to: Cook
The shop says: pad thai takes 8 minutes.
    (642 tokens, 16.5 seconds)

🇬🇧 English

The order went to the cashier and the question to the cook, and each answer came from that employee's tool. The first answer took over a minute, and the second about a quarter of that: the first had to load the 9 GB model, on a laptop with no graphics card.

Why not use the small model for the boss as well? We tried, with six questions for each.

🇹🇭 ไทย

รายการสั่งไปที่แคชเชียร์ และคำถามไปที่พ่อครัว คำตอบแต่ละข้อมาจากเครื่องมือของพนักงานคนนั้น คำตอบแรกใช้เวลากว่าหนึ่งนาที และคำตอบที่สองราวหนึ่งในสี่ของเวลานั้น: ครั้งแรกต้องโหลดโมเดลขนาด 9 GB บนแล็ปท็อปที่ไม่มีการ์ดจอ

ทำไมไม่ใช้โมเดลตัวเล็กกับหัวหน้าด้วย เราลองแล้ว ด้วยคำถามหกข้อสำหรับแต่ละตัว

🇨🇳 中文

订单交给了收银员,问题交给了厨师,两个回答都出自各自员工的工具。第一个回答用了一分多钟,第二个大约是它的四分之一:第一次要在一台没有显卡的笔记本上加载 9 GB 的模型。

为什么不让老板也用小模型?我们试过,每个模型各问六个问题。

The boss thinks with qwen2.5:3b
    2 khao soi, 1 water                    -> nobody   WRONG   said: Delegate to the Cashier with the provided or
    How long does pad thai take?           -> nobody   WRONG   said: (nothing)
    1 pad thai, 2 thai iced tea            -> nobody   WRONG   said: Delegate to member cashier with the provided
    How many minutes for khao soi?         -> nobody   WRONG   said: (nothing)
    3 water                                -> nobody   WRONG   said: delegate_task_to_member {"member_id": "cook"
    How long does nam ngiao take to cook?  -> nobody   WRONG   said: (nothing)
    0 of 6 right, in 8 seconds
The boss thinks with qwen2.5:14b
    2 khao soi, 1 water                    -> Cashier  right   said: - Total: 130 baht
    How long does pad thai take?           -> Cook     right   said: pad thai takes 8 minutes.
    1 pad thai, 2 thai iced tea            -> Cashier  right   said: - Total: 120 baht
    How many minutes for khao soi?         -> Cook     right   said: khao soi takes 6 minutes.
    3 water                                -> Cashier  right   said: - Total: 30 baht
    How long does nam ngiao take to cook?  -> Cook     right   said: nam ngiao takes 7 minutes.
    6 of 6 right, in 102 seconds
THE SECOND THING THAT WENT WRONG: A BOSS TOO SMALL FOR THE JOB With the small model as the boss, not one of the six questions reached an employee, and for three of them the reply was the text of the instruction it should have carried out. The same small model does the employees' work without trouble. Choosing between people turned out to be the harder job, so the boss gets the bigger model, and the six came out right. Two more settings in shop.py came from the same trials. route passes the employee's answer through untouched. And determine_input_for_members=False hands the employee the customer's own words, because the boss's retelling of an order had dropped the dishes.เมื่อให้โมเดลตัวเล็กเป็นหัวหน้า ไม่มีคำถามข้อใดในหกข้อไปถึงพนักงานเลย และมีสามข้อที่คำตอบเป็นข้อความของคำสั่งที่มันควรจะลงมือทำ โมเดลตัวเล็กตัวเดียวกันทำงานของพนักงานได้ไม่มีปัญหา การเลือกระหว่างคนกลับเป็นงานที่ยากกว่า หัวหน้าจึงได้โมเดลตัวใหญ่ และทั้งหกข้อก็ถูก การตั้งค่าอีกสองอย่างใน shop.py มาจากการทดลองชุดเดียวกัน route ส่งคำตอบของพนักงานผ่านไปโดยไม่แตะ และ determine_input_for_members=False ส่งคำของลูกค้าเองให้พนักงาน เพราะตอนที่หัวหน้าเล่ารายการสั่งใหม่ ชื่ออาหารหายไป让小模型当老板时,六个问题没有一个送到员工手里,其中三个的回复竟是它本该执行的那条指令的文字。同一个小模型做员工的活却毫无问题。原来,在人和人之间做选择才是更难的工作,所以老板用了大模型,六个问题全对。shop.py 里还有两处设置出自同一批试验。route 把员工的回答原封不动地传出去。determine_input_for_members=False 则把顾客的原话交给员工,因为老板转述订单时,把菜名弄丢了。

📏 Tutorial 5 — The rules the manager keeps · บทที่ 5 — กฎที่ผู้จัดการรักษาไว้ · 教程 5 —— 管理者守着的规矩

🇬🇧 English

So far the manager passes messages along. The reason to have one is the rules it keeps while you are not looking: what an agent may not do alone, when it must stop, and what is written down.

🇹🇭 ไทย

ถึงตรงนี้ผู้จัดการแค่ส่งข้อความต่อ เหตุผลที่ควรมีผู้จัดการคือกฎที่มันรักษาไว้ตอนที่คุณไม่ได้ดู: อะไรที่เอเจนต์ทำเองคนเดียวไม่ได้ เมื่อไรที่มันต้องหยุด และอะไรที่ถูกจดไว้

🇨🇳 中文

到这里,管理者还只是在传话。需要它的理由,是你不在场时它守着的那些规矩:智能体不能独自做什么,什么时候必须停下,以及哪些事会被记下来。

7 approve.py: wait for the owner's yes · approve.py: รอคำว่า “ได้” จากเจ้าของ · approve.py:等老板点头

🇬🇧 English

Giving money back is not for an agent to decide. In shop.py the tool refund is marked requires_confirmation=True. When the agent asks for it, Agno does not run it. The run stops with the status PAUSED and a list of what it is waiting for. Your code answers each one with confirm() or reject(), and continue_run() carries on from there.

🇹🇭 ไทย

การคืนเงินไม่ใช่เรื่องที่เอเจนต์จะตัดสินเอง ใน shop.py เครื่องมือ refund ถูกกำหนดเป็น requires_confirmation=True เมื่อเอเจนต์ขอใช้ Agno จะไม่รันมัน การรันหยุดด้วยสถานะ PAUSED พร้อมรายการสิ่งที่มันรออยู่ โค้ดของคุณตอบแต่ละรายการด้วย confirm() หรือ reject() แล้ว continue_run() จะทำต่อจากตรงนั้น

🇨🇳 中文

退钱这件事,不该由智能体自己决定。在 shop.py 里,工具 refund 标上了 requires_confirmation=True。智能体要求调用它时,Agno 并不运行它。这次运行停下来,状态是 PAUSED,并附上它在等什么的清单。你的代码用 confirm() 或 reject() 逐项回答,然后 continue_run() 从那里接着往下走。

"""approve.py - an agent asks to give money back, and waits for the owner's answer.

    python approve.py yes
    python approve.py no
"""
import sys

from shop import books, refunds


def main():
    owner_says = sys.argv[1]
    agent = refunds(books())
    run = agent.run("Table 4 had a cold khao soi. Give them back 60 baht.",
                    session_id="refund-" + owner_says)
    print("Status:", run.status.value)
    for need in run.active_requirements:
        asked = need.tool_execution
        print("The agent wants to call:", asked.tool_name, asked.tool_args)
        if owner_says == "yes":
            need.confirm()
        else:
            need.reject("The owner said no.")
    print("The owner says:", owner_says)

    # The run was saved in the books when it paused. This picks it up again.
    run = agent.continue_run(run_id=run.run_id, session_id=run.session_id,
                             requirements=run.requirements)
    print("Status:", run.status.value)
    print("Refunds says:", run.content)


if __name__ == "__main__":
    main()
venv\Scripts\python approve.py yes
venv\Scripts\python approve.py no
Status: PAUSED
The agent wants to call: refund {'baht': 60}
The owner says: yes
Status: COMPLETED
Refunds says: I have refunded 60 baht to Table 4.
Status: PAUSED
The agent wants to call: refund {'baht': 60}
The owner says: no
Status: COMPLETED
Refunds says: It seems the owner did not agree to give you the refund. Please check with the owner again.

🇬🇧 English

Here the yes comes from the command line, which stands in for the owner. In a real shop it would come from a button or a chat message, hours later. That works because the paused run is in the books, and continue_run() finds it there by its number. It needs the session's name as well, and it needs the agent to have a database: without one it told us that no run with that number exists.

🇹🇭 ไทย

ในที่นี้คำว่า “ได้” มาจากบรรทัดคำสั่ง ซึ่งทำหน้าที่แทนเจ้าของ ในร้านจริงมันจะมาจากปุ่มหรือข้อความแชต หลายชั่วโมงให้หลัง ทำแบบนั้นได้เพราะการรันที่หยุดรออยู่ในสมุดบัญชี และ continue_run() หามันเจอที่นั่นด้วยหมายเลขของมัน มันต้องการชื่อของเซสชันด้วย และเอเจนต์ต้องมีฐานข้อมูล: เมื่อไม่มี มันบอกเราว่าไม่มีการรันที่มีหมายเลขนั้น

🇨🇳 中文

这里的“行”来自命令行,它代替了老板。在真正的店里,它会来自一个按钮或一条聊天消息,也许是几个小时以后。这之所以行得通,是因为暂停的那次运行就在账本里,continue_run() 凭它的编号在那里找到它。它还需要会话的名字,并且智能体必须有数据库:没有数据库时,它告诉我们不存在这个编号的运行。

8 budget.py: a budget that stops an agent · budget.py: งบประมาณที่หยุดเอเจนต์ได้ · budget.py:能让智能体停下来的预算

🇬🇧 English

Our models are free, so nothing here costs money. An agent with a paid model inside can spend it fast, and the unit it spends is the token. Agno counts the tokens of every run. A budget is then a few lines of your own: a hook, which Agno calls before each run, adds up what the conversation has used and refuses the run when that is too much.

🇹🇭 ไทย

โมเดลของเราฟรี ในหน้านี้จึงไม่มีอะไรเสียเงิน แต่เอเจนต์ที่มีโมเดลเสียเงินอยู่ข้างในใช้เงินได้เร็ว และหน่วยที่มันใช้คือโทเคน Agno นับโทเคนของทุกการรัน งบประมาณจึงเป็นโค้ดของคุณเองไม่กี่บรรทัด: hook ซึ่ง Agno เรียกก่อนการรันแต่ละครั้ง จะรวมสิ่งที่บทสนทนาใช้ไปแล้ว และปฏิเสธการรันเมื่อมากเกินไป

🇨🇳 中文

我们的模型是免费的,所以这里什么都不花钱。但里面装着收费模型的智能体花钱很快,它花的单位就是 token。Agno 会统计每次运行的 token。于是预算就是你自己写的几行代码:一个 hook,Agno 在每次运行之前调用它,它把这段对话已经用掉的加起来,超了就拒绝这次运行。

"""budget.py - a budget of tokens. When it is spent, the cashier takes no more orders.

    python budget.py

A token is a piece of a word. It is the unit a paid model is billed in, so a
budget of tokens is a budget of money.
"""
from agno.exceptions import InputCheckError

from shop import books, cashier

BUDGET = 800            # tokens that one conversation may use
TABLE = "table-9"
ORDERS = ["1 khao soi", "2 water", "1 pad see ew, 1 longan juice", "3 thai iced tea",
          "1 nam ngiao"]


def spent(agent):
    """The tokens this conversation has used so far, read from the books."""
    session = agent.get_session(session_id=TABLE)
    if session is None:
        return 0
    return sum(run.metrics.total_tokens for run in session.runs)


def check_budget(agent):
    """Agno calls this before every run, and hands it the agent."""
    if spent(agent) >= BUDGET:
        # of all the errors a hook can raise, this is the kind that stops the run
        raise InputCheckError(f"The budget of {BUDGET} tokens is spent.")


def main():
    agent = cashier(books())
    agent.pre_hooks = [check_budget]
    for order in ORDERS:
        run = agent.run(order, session_id=TABLE)
        print(f"{order}: {run.status.value}. {run.content.splitlines()[-1]}")
        print(f"    spent {spent(agent)} of {BUDGET} tokens")


if __name__ == "__main__":
    main()
venv\Scripts\python budget.py
1 khao soi: COMPLETED. - Total: 60 baht
    spent 235 of 800 tokens
2 water: COMPLETED. - Total: 20 baht
    spent 527 of 800 tokens
1 pad see ew, 1 longan juice: COMPLETED. - Total: 80 baht
    spent 899 of 800 tokens
ERROR   Validation failed: The budget of 800 tokens is spent. | Check trigger: CheckTrigger.INPUT_NOT_ALLOWED
3 thai iced tea: ERROR. The budget of 800 tokens is spent.
    spent 899 of 800 tokens
ERROR   Validation failed: The budget of 800 tokens is spent. | Check trigger: CheckTrigger.INPUT_NOT_ALLOWED
1 nam ngiao: ERROR. The budget of 800 tokens is spent.
    spent 899 of 800 tokens

🇬🇧 English

Three orders went through and two were refused. The third took the total past the budget, because the hook looks before a run and cannot know what that run will cost. The lines that begin with ERROR are Agno's own: it prints one for each run a hook refuses. Notice also that each order cost more tokens than the one before. The cashier is shown the conversation so far, and it is charged for reading it again.

🇹🇭 ไทย

มีรายการสั่งสามรายการที่ผ่าน และสองรายการถูกปฏิเสธ รายการที่สามทำให้ยอดรวมเกินงบ เพราะ hook ตรวจก่อนการรัน และรู้ไม่ได้ว่าการรันครั้งนั้นจะใช้เท่าไร บรรทัดที่ขึ้นต้นด้วย ERROR เป็นของ Agno เอง: มันพิมพ์หนึ่งบรรทัดต่อการรันแต่ละครั้งที่ hook ปฏิเสธ สังเกตด้วยว่ารายการสั่งแต่ละครั้งใช้โทเคนมากกว่าครั้งก่อน แคชเชียร์ได้เห็นบทสนทนาจนถึงตอนนั้น และต้องจ่ายค่าอ่านมันอีกรอบ

🇨🇳 中文

三份订单通过了,两份被拒绝。第三份让总数超出了预算,因为 hook 是在运行之前检查的,它无从知道这次运行会花多少。以 ERROR 开头的那些行是 Agno 自己打印的:hook 每拒绝一次运行,它就打印一行。还要注意,每份订单用掉的 token 都比上一份多。收银员会看到到目前为止的对话,重读一遍也是要算 token 的。

ERROR   Pre-hook #1 execution failed
refuse_with_stop:
    status: COMPLETED
    the cashier said: - Total: 60 baht
ERROR   Validation failed: The budget is spent. | Check trigger: CheckTrigger.INPUT_NOT_ALLOWED
refuse_with_check:
    status: ERROR
    the cashier said: The budget is spent.
THE THIRD THING THAT WENT WRONG: AN ERROR THAT STOPS NOTHING Agno has an error named StopAgentRun, and our first hook raised it. Agno printed that the hook had failed, and then ran the order anyway: the status was COMPLETED and the customer got a bill. Only InputCheckError, raised in a hook, stops the run. A budget that prints an error and stops nothing is worse than no budget, so test yours by spending it.Agno มีข้อผิดพลาดชื่อ StopAgentRun และ hook ตัวแรกของเรา raise ตัวนี้ Agno พิมพ์ว่า hook ล้มเหลว แล้วก็รันรายการสั่งต่อไปอยู่ดี: สถานะเป็น COMPLETED และลูกค้าได้บิล มีเพียง InputCheckError ที่ raise ใน hook เท่านั้นที่หยุดการรันได้ งบประมาณที่พิมพ์ข้อผิดพลาดแต่ไม่หยุดอะไรเลยนั้นแย่กว่าไม่มีงบประมาณ ดังนั้นให้ทดสอบงบของคุณด้วยการใช้มันจนหมดAgno 有一个叫 StopAgentRun 的错误,我们的第一个 hook 抛出的就是它。Agno 打印说这个 hook 失败了,然后照样运行了订单:状态是 COMPLETED,顾客拿到了账单。在 hook 里抛出的错误中,只有 InputCheckError 能让运行停下。一份只会打印错误、什么也拦不住的预算,比没有预算还糟。所以要把预算真的花光一次,来测试它。

9 runs.py: everything is written down · runs.py: ทุกอย่างถูกจดไว้ · runs.py:一切都有记录

🇬🇧 English

Every file since step 4 has been writing in shop.db. This one starts no model. It opens the books and prints what is there: each conversation, each run in it, its status, its tokens, and the tools it used.

🇹🇭 ไทย

ทุกไฟล์ตั้งแต่ขั้นที่ 4 เขียนลงใน shop.db มาตลอด ไฟล์นี้ไม่เริ่มโมเดลใดเลย มันเปิดสมุดบัญชีแล้วพิมพ์สิ่งที่อยู่ในนั้น: แต่ละบทสนทนา แต่ละการรันในนั้น สถานะ โทเคน และเครื่องมือที่ใช้

🇨🇳 中文

从第 4 步起,每个文件都在往 shop.db 里写东西。这个文件不启动任何模型。它打开账本,把里面的内容打印出来:每一段对话,其中的每一次运行,它的状态、token 数和用过的工具。

"""runs.py - read the books: every conversation, every run, every tool call.

    python runs.py

It starts no model. It only reads shop.db, the file the agents wrote.
"""
from agno.db.base import SessionType

from shop import books


def main():
    db = books()
    sessions = db.get_sessions(session_type=SessionType.AGENT)
    print(len(sessions), "conversation(s) in the books.")
    for session in sessions:
        runs = session.runs or []
        print(f"{session.session_id}: {len(runs)} run(s) by {session.agent_id}")
        total = 0
        for run in runs:
            calls = [call.tool_name for call in run.tools or []]
            total = total + run.metrics.total_tokens
            print(f"    {run.status:10} {run.metrics.total_tokens:5} tokens   "
                  f"tools: {', '.join(calls) or 'none'}   said: {str(run.input.input_content)[:40]}")
        print(f"    {total} tokens in all")


if __name__ == "__main__":
    main()
venv\Scripts\python runs.py
4 conversation(s) in the books.
table-4: 3 run(s) by cashier
    COMPLETED    249 tokens   tools: bill   said: 2 khao soi, 1 thai iced tea
    COMPLETED    333 tokens   tools: bill   said: 1 pad thai, 2 pizza
    COMPLETED    404 tokens   tools: bill   said: Sorry, no pizza. Make that 1 pad thai, 2
    986 tokens in all
refund-yes: 1 run(s) by refunds
    COMPLETED    490 tokens   tools: refund   said: Table 4 had a cold khao soi. Give them b
    490 tokens in all
refund-no: 1 run(s) by refunds
    COMPLETED    494 tokens   tools: refund   said: Table 4 had a cold khao soi. Give them b
    494 tokens in all
table-9: 5 run(s) by cashier
    COMPLETED    235 tokens   tools: bill   said: 1 khao soi
    COMPLETED    292 tokens   tools: bill   said: 2 water
    COMPLETED    372 tokens   tools: bill   said: 1 pad see ew, 1 longan juice
    ERROR          0 tokens   tools: none   said: 3 thai iced tea
    ERROR          0 tokens   tools: none   said: 1 nam ngiao
    899 tokens in all

🇬🇧 English

The two refused orders are there too, with status ERROR, no tokens and no tools: a refusal costs nothing and is still recorded. The file is an ordinary SQLite database, so any program that reads SQLite can read it, with or without Agno.

🇹🇭 ไทย

รายการสั่งสองรายการที่ถูกปฏิเสธก็อยู่ในนั้นด้วย สถานะ ERROR ไม่มีโทเคน ไม่มีเครื่องมือ: การปฏิเสธไม่มีค่าใช้จ่าย และยังถูกบันทึกไว้ ไฟล์นี้เป็นฐานข้อมูล SQLite ธรรมดา โปรแกรมใดก็ตามที่อ่าน SQLite ได้จึงอ่านมันได้ จะมี Agno หรือไม่ก็ตาม

🇨🇳 中文

被拒绝的那两份订单也在里面,状态是 ERROR,没有 token,没有工具:拒绝不花任何成本,却仍然被记了下来。这个文件是一个普通的 SQLite 数据库,所以任何能读 SQLite 的程序都能读它,有没有 Agno 都一样。

🛰️ Tutorial 6 — The manager as a server · บทที่ 6 — ผู้จัดการในรูปเซิร์ฟเวอร์ · 教程 6 —— 把管理者做成服务器

10 serve.py and client.py · serve.py และ client.py · serve.py 和 client.py

🇬🇧 English

Until now, using the shop meant importing shop.py. A website, a phone app or a program in another language cannot do that. AgentOS takes the same agents and the same team and puts them behind web addresses. It is a FastAPI app, and uvicorn runs it.

🇹🇭 ไทย

จนถึงตอนนี้ การใช้ร้านหมายถึงการ import shop.py เว็บไซต์ แอปมือถือ หรือโปรแกรมในภาษาอื่นทำแบบนั้นไม่ได้ AgentOS นำเอเจนต์ชุดเดิมและทีมเดิมไปวางไว้หลังที่อยู่เว็บ มันเป็นแอป FastAPI และ uvicorn เป็นตัวรันมัน

🇨🇳 中文

到现在为止,要用这家店,就得 import shop.py。网站、手机应用或别的语言写的程序做不到这一点。AgentOS 把同样的智能体和同一个团队放到一组网址后面。它是一个 FastAPI 应用,由 uvicorn 来运行。

"""serve.py - the shop as a server: Agno's AgentOS, on this computer only.

    python serve.py         leave it running, and use client.py in a second terminal

Stop it with Ctrl+C.
"""
import uvicorn
from agno.os import AgentOS

from shop import books, cashier, cook, refunds, team


def main():
    db = books()
    manager = AgentOS(
        name="Three Geckos Noodles",
        agents=[cashier(db), cook(db), refunds(db)],
        teams=[team(db)],
        db=db,
        # Agno reports anonymous usage numbers to its makers unless this is False
        telemetry=False,
    )
    # 127.0.0.1 is this computer talking to itself: nothing else on the network can reach it
    uvicorn.run(manager.get_app(), host="127.0.0.1", port=7777)


if __name__ == "__main__":
    main()

🇬🇧 English

Start it and leave it running. In a second terminal, run the client. The client imports nothing from Agno: it sends ordinary web requests, as a program in any language could.

🇹🇭 ไทย

เริ่มมันแล้วปล่อยให้รันไว้ ในเทอร์มินัลที่สอง ให้รันไคลเอนต์ ไคลเอนต์ไม่ได้ import อะไรจาก Agno เลย: มันส่งคำขอเว็บธรรมดา อย่างที่โปรแกรมในภาษาใดก็ทำได้

🇨🇳 中文

启动它,让它一直运行着。在第二个终端里运行客户端。客户端没有从 Agno 里 import 任何东西:它发出的是普通的网络请求,任何语言写的程序都能这样做。

"""client.py - talk to the shop's server from another program, with plain requests.

    python client.py "1 nam ngiao, 1 water"

serve.py must be running in another terminal. This file imports nothing from
Agno: any program, in any language, could send these requests.
"""
import sys

import requests

API = "http://127.0.0.1:7777"


def main():
    order = sys.argv[1]
    print("The server says:", requests.get(API + "/health").json()["status"])

    print("Agents on it:")
    for agent in requests.get(API + "/agents").json():
        tools = [one["name"] for one in agent["tools"]["tools"]]
        print("   ", agent["id"], "-", agent["role"], "- tools:", ", ".join(tools))
    for team in requests.get(API + "/teams").json():
        print("Team:", team["id"], "- mode:", team["mode"])

    # The order goes in as form fields. stream=false: wait, and get one whole answer.
    form = {"message": order, "stream": "false", "session_id": "table-7"}
    run = requests.post(API + "/agents/cashier/runs", data=form).json()
    print("Run", run["status"], "by", run["agent_id"], "in", run["session_id"] + ":")
    print(run["content"])
    print("   ", run["metrics"]["total_tokens"], "tokens")

    sessions = requests.get(API + "/sessions", params={"type": "agent"}).json()["data"]
    print("Conversations the server knows:")
    for session in sessions:
        print("   ", session["session_id"], "-", session["agent_id"], "-",
              session["total_tokens"], "tokens")


if __name__ == "__main__":
    main()
venv\Scripts\python serve.py
venv\Scripts\python client.py "1 nam ngiao, 1 water"
The server says: ok
Agents on it:
    cashier - Adds up the bill for an order - tools: bill
    cook - Knows how long each dish takes to cook - tools: cooking_time
    refunds - Gives money back, when the owner agrees - tools: refund
Team: three-geckos - mode: route
Run COMPLETED by cashier in table-7:
- 1 x nam ngiao = 50 baht
- 1 x water = 10 baht
- Total: 60 baht
    241 tokens
Conversations the server knows:
    table-7 - cashier - 241 tokens
    table-9 - cashier - 899 tokens
    refund-no - refunds - 720 tokens
    refund-yes - refunds - 716 tokens
    table-4 - cashier - 986 tokens

🇬🇧 English

The server knew about table 4, the refunds and table 9 although it had only just started. It reads the same books. That is the whole design: the agents are Python objects, the records are one file, and the server is a way in.

🇹🇭 ไทย

เซิร์ฟเวอร์รู้เรื่องโต๊ะ 4 การคืนเงิน และโต๊ะ 9 ทั้งที่เพิ่งเริ่มทำงาน เพราะมันอ่านสมุดบัญชีเล่มเดียวกัน นั่นคือการออกแบบทั้งหมด: เอเจนต์คือออบเจกต์ Python บันทึกคือไฟล์เดียว และเซิร์ฟเวอร์คือทางเข้า

🇨🇳 中文

服务器才刚启动,却已经知道 4 号桌、那两笔退款和 9 号桌的事。它读的是同一本账。这就是全部的设计:智能体是 Python 对象,记录是一个文件,服务器是一个入口。

TWO THINGS TO KNOW BEFORE YOU PUT THIS ANYWHERE ELSE The server listens on 127.0.0.1, which is your computer talking to itself. Nothing else can reach it, and it asks nobody to log in. Do not change that address to let other computers in until you have read what Agno's documentation says about authentication. And Agno's code switches telemetry on unless you say otherwise: that is what telemetry=False in serve.py is for.เซิร์ฟเวอร์รับฟังที่ 127.0.0.1 ซึ่งคือคอมพิวเตอร์ของคุณคุยกับตัวเอง ไม่มีเครื่องอื่นเข้าถึงได้ และมันไม่ขอให้ใครล็อกอิน อย่าเปลี่ยนที่อยู่นั้นเพื่อให้เครื่องอื่นเข้ามา จนกว่าคุณจะได้อ่านสิ่งที่เอกสารของ Agno เขียนเรื่องการยืนยันตัวตน และโค้ดของ Agno เปิด telemetry ไว้ เว้นแต่คุณจะบอกเป็นอย่างอื่น: นั่นคือหน้าที่ของ telemetry=False ใน serve.py服务器监听的是 127.0.0.1,也就是你的电脑在跟自己说话。别的机器都连不上它,它也不要求任何人登录。在读过 Agno 文档里关于身份验证的内容之前,不要为了让别的电脑连进来而改掉这个地址。另外,除非你另行指定,Agno 的代码默认开着 telemetry:serve.py 里的 telemetry=False 就是为此而写的。
YOUR TURN
  1. Add a dish and its price to PRICES in shop.py, and order it through client.py. Did you have to restart the server?
  2. Change BUDGET in budget.py to 300, delete shop.db, and run it. How many orders go through now?
  3. Ask the team something neither employee can answer, such as the opening hours. Who does Noi give it to?
  1. เพิ่มอาหารหนึ่งอย่างกับราคาลงใน PRICES ใน shop.py แล้วสั่งผ่าน client.py คุณต้องเริ่มเซิร์ฟเวอร์ใหม่หรือไม่
  2. เปลี่ยน BUDGET ใน budget.py เป็น 300 ลบ shop.db แล้วรัน คราวนี้มีรายการสั่งผ่านกี่รายการ
  3. ถามทีมเรื่องที่พนักงานทั้งสองคนตอบไม่ได้ เช่นเวลาเปิดร้าน น้อยส่งให้ใคร
  1. 在 shop.py 的 PRICES 里加一道菜和它的价格,再通过 client.py 点它。你需要重启服务器吗?
  2. 把 budget.py 里的 BUDGET 改成 300,删掉 shop.db,再运行。这回有几份订单能通过?
  3. 向团队问一个两个员工都答不了的问题,比如营业时间。Noi 把它交给了谁?

🇬🇧 English

How does this compare with Paperclip? Paperclip gave us more without code: an org chart in a browser, a rule that one task belongs to one agent, and budgets that pause an agent by themselves. With Agno each of those is something you write, as step 8 did for the budget. What you get in return is that the manager is Python from top to bottom. There is no second program to install and keep running, and a rule is a function you can read and test.

Next: smolagents shows an agent that writes its own Python, and LangGraph draws the same kind of team as a graph. Python to EXE packs a Python program so that someone without Python can run it.

🇹🇭 ไทย

เทียบกับ Paperclip แล้วเป็นอย่างไร Paperclip ให้เรามากกว่าโดยไม่ต้องเขียนโค้ด: ผังองค์กรในเบราว์เซอร์ กฎที่ว่างานหนึ่งงานเป็นของเอเจนต์ตัวเดียว และงบประมาณที่พักเอเจนต์ได้เอง ใน Agno แต่ละอย่างนี้เป็นสิ่งที่คุณต้องเขียน อย่างที่ขั้นที่ 8 ทำกับงบประมาณ สิ่งที่ได้กลับมาคือผู้จัดการเป็น Python ตั้งแต่บนจรดล่าง ไม่มีโปรแกรมที่สองให้ติดตั้งและคอยเปิดไว้ และกฎหนึ่งข้อคือฟังก์ชันที่คุณอ่านและทดสอบได้

ต่อไป: smolagents แสดงเอเจนต์ที่เขียน Python ของตัวเอง และ LangGraph วาดทีมแบบเดียวกันเป็นกราฟ ส่วน Python to EXE แพ็กโปรแกรม Python ให้คนที่ไม่มี Python รันได้

🇨🇳 中文

这和 Paperclip 比起来怎么样?Paperclip 不用写代码就给了我们更多:浏览器里的组织架构图,一个任务只属于一个智能体的规矩,以及会自动暂停智能体的预算。在 Agno 里,这些每一样都得你自己写,就像第 8 步写预算那样。换来的是:管理者从头到尾都是 Python。没有第二个程序要安装、要一直开着,一条规矩就是一个你读得懂、测得了的函数。

接下来:smolagents 展示一个自己写 Python 的智能体,LangGraph 把同一类团队画成一张图。Python to EXE 则把 Python 程序打包,让没有 Python 的人也能运行。

🔬 Tutorial 7 — A research paper assistant · บทที่ 7 — ผู้ช่วยทำรายงานวิจัย · 教程 7 —— 研究报告助手

🇬🇧 English

The shop's agents read orders. This last tutorial gives agents harder reading: a research paper, the kind a software company writes before it decides what to build. Three agents work on it. A reviewer gives it a quality score from 1 to 100. A researcher searches the web for the facts it lacks. A writer writes it again. A loop sends the paper round those three until the score reaches a target, or the rounds run out.

It is the team of Tutorial 5 of the Paperclip page, with the same draft paper, built the Agno way. There a fourth agent, the overseer, was a program that Paperclip woke. Here the overseer is the loop in main(), because with Agno the manager is your own Python.

🇹🇭 ไทย

เอเจนต์ของร้านอ่านรายการสั่ง บทสุดท้ายนี้ให้เอเจนต์อ่านสิ่งที่ยากขึ้น: รายงานวิจัย แบบที่บริษัทซอฟต์แวร์เขียนก่อนตัดสินใจว่าจะสร้างอะไร มีเอเจนต์สามตัวทำงานกับมัน ผู้รีวิวให้คะแนนคุณภาพจาก 1 ถึง 100 นักค้นคว้าค้นเว็บหาข้อเท็จจริงที่รายงานยังขาด นักเขียนเขียนรายงานใหม่ และลูปส่งรายงานวนไปหาสามตัวนั้นจนกว่าคะแนนจะถึงเป้า หรือครบจำนวนรอบ

มันคือทีมจากบทที่ 5 ของหน้า Paperclip กับรายงานฉบับร่างเดียวกัน แต่สร้างในแบบของ Agno ที่นั่นเอเจนต์ตัวที่สี่ คือผู้คุมงาน เป็นโปรแกรมที่ Paperclip ปลุกขึ้นมา ที่นี่ผู้คุมงานคือลูปใน main() เพราะใน Agno ผู้จัดการคือ Python ของคุณเอง

🇨🇳 中文

店里的智能体读的是订单。最后这个教程让智能体读更难的东西:一篇研究报告,就是软件公司在决定做什么之前写的那种。有三个智能体处理它。审稿人给它打一个 1 到 100 的质量分。研究员上网搜索它缺少的事实。写手把它重写一遍。一个循环让报告在这三者之间转,直到分数达到目标,或者轮数用完。

它就是 Paperclip 那一页教程 5 里的团队,用的是同一篇初稿,只是按 Agno 的方式搭建。在那里,第四个智能体,也就是监督员,是一个由 Paperclip 叫醒的程序。在这里,监督员就是 main() 里的那个循环,因为在 Agno 里,管理者就是你自己的 Python。

11 research.py: three agents and a loop · research.py: เอเจนต์สามตัวกับลูปหนึ่งลูป · research.py:三个智能体和一个循环

🇬🇧 English

One more library first. ddgs asks public search engines a question and returns the first results. It needs no account and no key. The paper is paper.txt, a short research paper for a software team that asks whether the team should write its tests before its code. It is weak on purpose: three paragraphs, an opinion, and not one number or source.

Three things in the file are new. Review is a form. It is a small class that says the reviewer's answer must hold a whole number from 1 to 100 and three sentences. Handing it to the agent as output_schema makes Agno ask the model for exactly that, and what comes back is a Python object, so review.score is a number that the loop can compare. search_web is a tool marked stop_after_tool_call, as the bill was in step 4: the model chooses the search, and the notes travel on exactly as the search engine gave them. And the loop keeps the best paper it has seen, because a rewrite can be worse than what it replaced.

🇹🇭 ไทย

ต้องมีไลบรารีอีกหนึ่งตัวก่อน ddgs ถามเสิร์ชเอนจินสาธารณะแล้วคืนผลลัพธ์แรก ๆ กลับมา ไม่ต้องมีบัญชีและไม่ต้องมีคีย์ รายงานคือ paper.txt รายงานวิจัยสั้น ๆ สำหรับทีมซอฟต์แวร์ ที่ถามว่าทีมควรเขียนเทสต์ก่อนเขียนโค้ดหรือไม่ เราตั้งใจให้มันอ่อน คือมีสามย่อหน้า มีความเห็น และไม่มีตัวเลขหรือแหล่งที่มาเลยสักอย่าง

ในไฟล์มีของใหม่สามอย่าง Review คือแบบฟอร์ม มันเป็นคลาสเล็ก ๆ ที่บอกว่าคำตอบของผู้รีวิวต้องมีจำนวนเต็มจาก 1 ถึง 100 กับประโยคสามประโยค การส่งมันให้เอเจนต์เป็น output_schema ทำให้ Agno ขอสิ่งนั้นจากโมเดลตรงตามแบบ และสิ่งที่กลับมาคือออบเจกต์ Python ดังนั้น review.score จึงเป็นตัวเลขที่ลูปนำไปเปรียบเทียบได้ search_web คือเครื่องมือที่กำหนด stop_after_tool_call เหมือนบิลในขั้นที่ 4: โมเดลเลือกคำค้น แล้วโน้ตเดินทางต่อไปตรงตามที่เสิร์ชเอนจินให้มา และลูปเก็บรายงานที่ดีที่สุดเท่าที่เคยเห็นไว้ เพราะฉบับที่เขียนใหม่อาจแย่กว่าฉบับที่มันมาแทน

🇨🇳 中文

先再装一个库。ddgs 向公开的搜索引擎提问,返回最前面的几条结果。它不需要账号,也不需要密钥。报告是 paper.txt,一篇写给软件团队的简短研究报告,问的是团队该不该先写测试、再写代码。它是故意写得弱的:三段话,一个观点,没有一个数字,也没有一处出处。

文件里有三样新东西。Review 是一张表格。它是一个很小的类,规定审稿人的回答必须包含一个 1 到 100 的整数和三句话。把它作为 output_schema 交给智能体,Agno 就会要求模型严格按这个格式回答,而返回的是一个 Python 对象,所以 review.score 是一个循环可以拿来比较的数字。search_web 是一件标了 stop_after_tool_call 的工具,和第 4 步的账单一样:搜什么由模型决定,笔记则原封不动地按搜索引擎给的样子往下传。循环还会留住它见过的最好的那篇报告,因为重写出来的可能还不如原来的。

"""research.py - a research paper assistant for a software team: three agents and a loop.

    python research.py paper.txt

A reviewer gives the paper a quality score from 1 to 100. A researcher searches
the web for the facts it lacks. A writer writes it again. The loop in main()
sends the paper round those three until the score reaches TARGET, or the rounds
run out. Ollama must be running.
"""
import sys
import time
from pathlib import Path

from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.ollama import Ollama
from agno.tools import tool
from ddgs import DDGS
from ddgs.exceptions import DDGSException
from pydantic import BaseModel, Field

MODEL = "qwen2.5:3b"
TARGET = 85             # stop when the reviewer gives this score
ROUNDS = 3              # stop after this many reviews
DB_FILE = "research.db"


class Review(BaseModel):
    """The form the reviewer has to fill in. Agno makes the model keep to it."""
    score: int = Field(ge=1, le=100, description="the quality of the paper, from 1 to 100")
    fixes: list[str] = Field(min_length=3, max_length=3,
                             description="three sentences, each naming a thing to fix")


# stop_after_tool_call: the notes go back as the search engine gave them. The
# model chooses the search, and it does not retell the pages.
@tool(stop_after_tool_call=True)
def search_web(search: str) -> str:
    """Search the web, and get the first three results with their addresses.

    Args:
        search: a short web search, such as "test-driven development defect study"
    """
    try:
        hits = DDGS().text(search, max_results=3)
    except DDGSException:       # the search found nothing, or was refused
        return "- The search found nothing. Add no new facts."
    return "\n".join("- " + hit["body"] + " (" + hit["href"] + ")" for hit in hits)


def team(db):
    """The three agents, by the name of their job."""
    # temperature 0: the same question gets the same answer.
    # num_ctx: room for the paper, the review and the notes together.
    model = Ollama(id=MODEL, options={"temperature": 0, "num_ctx": 8192})
    reviewer = Agent(
        name="Reviewer", id="reviewer", model=model, db=db, output_schema=Review,
        instructions="You review research papers for a software company. The people "
                     "who read them decide what the engineers build next. Give the "
                     "paper a quality score from 1 to 100. A paper "
                     "earns a high score when it asks one clear question, answers it "
                     "with evidence, says where each fact comes from, says what the "
                     "change would cost, answers the strongest objection, and ends "
                     "with one thing to do next.",
    )
    researcher = Agent(
        name="Researcher", id="researcher", model=model, db=db, tools=[search_web],
        instructions="You are given a paper and a review of it. Use the search_web "
                     "tool once, with one short search that would find the facts the "
                     "paper lacks.",
    )
    writer = Agent(
        name="Writer", id="writer", model=model, db=db,
        instructions="Write this paper again so that it earns a higher score. Fix "
                     "what the reviewer asks for. Take facts only from the notes, and "
                     "say in the sentence where each fact comes from. Keep what the "
                     "paper recommends. Answer with the paper only: its title, then "
                     "its paragraphs.",
    )
    return reviewer, researcher, writer


def main():
    paper = Path(sys.argv[1]).read_text(encoding="utf-8").strip()
    reviewer, researcher, writer = team(SqliteDb(db_file=DB_FILE))
    session = "paper-" + time.strftime("%H%M%S")       # one conversation for each try
    started = time.time()
    best_score = 0
    for turn in range(1, ROUNDS + 1):
        review = reviewer.run(paper, session_id=session).content
        print(f"Round {turn}. Score: {review.score}")
        for fix in review.fixes:
            print("- " + fix)
        print()
        if review.score > best_score:   # a rewrite can be worse: keep the best one
            best_score = review.score
            best_paper = paper
            best_turn = turn
        if review.score >= TARGET or turn == ROUNDS:
            break                       # good enough, or out of rounds: stop
        said = "\n".join(review.fixes)
        both = "THE PAPER\n" + paper + "\n\nTHE REVIEWER\n" + said
        notes = researcher.run(both, session_id=session).content
        print("Notes from the web:")
        print(notes)
        print()
        paper = writer.run(both + "\n\nNOTES FROM THE WEB\n" + notes,
                           session_id=session).content
    print(f"Best score: {best_score}, in round {best_turn} of {turn}. "
          f"The target was {TARGET}.")
    print()
    print(best_paper)
    print()
    print(f"That took {time.time() - started:.0f} seconds.")


if __name__ == "__main__":
    main()
Should our team write the tests first?

Our team writes the code and then writes tests later, if there is time. I think we should write the tests first. This is called test-driven development. It makes the code better and there are fewer bugs.

Writing tests first is a good idea because you think about what the code should do before you write it. Many companies do it. Our last release had a lot of bugs and customers were unhappy. Tests would have found them.

Some people say writing tests first is slow. But fixing bugs is slower. So we should write the tests first on every project.
venv\Scripts\python -m pip install ddgs
venv\Scripts\python research.py paper.txt
Round 1. Score: 45
- The paper does not clearly define what 
-  
-  should write tests for (e.g., unit tests, integration tests, etc.). It also does not specify the scope of the tests (e.g., all functions, just critical ones). The paper does not address the cost of writing tests first. It does not mention any objections to writing tests first. The paper ends with a recommendation to write tests first on every project, which is not a clear next step.

Notes from the web:
- This blog post by Seb Rose examines the prevalent objections to Test-Driven Development (TDD) and offers well-founded refutations. Drawing on data from a comprehensive survey, it categorizes 896 objections into five main areas: education, project constraints, cultural challenges, time... (https://www.slideserve.com/tirza/common-objections-to-tdd-and-their-refutations)
- May 29, 2023 · Learn what Test-Driven Development is, how it connects to testing, and why it can optimize your development process and reduce costs. (https://accedia.com/insights/blog/test-driven-development-key-for-reducing-development-costs)
- By introducing TDD in two projects of ISM we tried to determine whether TDD is cost-effective by determining the Return on Investment (ROI). This cost-effectiveness could, due to the insufficient dataset, only be assessed in a limited way. In the monitored projects the developers took 31% more time for developing work items. (https://repository.tudelft.nl/file/File_610a728a-f779-43c5-b02b-65dced138f16)

Round 2. Score: 25
- Clearly define what tests should be written and specify the scope of the tests.
- Address the cost of writing tests first.
- Address the strongest objection to writing tests first.

Notes from the web:
- Discover how Test-Driven Development can improve your software testing process with this comprehensive guide. Learn the benefits and best practices here. (https://www.practitest.com/resource-center/article/tdd-guide/)
- Test-driven development (TDD) is a software development approach in which automated tests are written before the production code they validate. The tests define the expected behavior, and the code evolves to satisfy them. (https://testsigma.com/blog/test-driven-development/)
- Test-Driven Development (TDD) is a Software development method in which you write Automation Tests before the actual development process starts, which is coding. Here we are learning TDD in detail with these important points related to the same. (https://www.geeksforgeeks.org/software-engineering/test-driven-development-tdd/)

Round 3. Score: 30
- Clearly define what tests should be written and specify the scope of the tests.
- Address the cost of writing tests first.
- Address the strongest objection to writing tests first.

Best score: 45, in round 1 of 3. The target was 85.

Should our team write the tests first?

Our team writes the code and then writes tests later, if there is time. I think we should write the tests first. This is called test-driven development. It makes the code better and there are fewer bugs.

Writing tests first is a good idea because you think about what the code should do before you write it. Many companies do it. Our last release had a lot of bugs and customers were unhappy. Tests would have found them.

Some people say writing tests first is slow. But fixing bugs is slower. So we should write the tests first on every project.

That took 136 seconds.

🇬🇧 English

That took 136 seconds on the laptop. The scores went 45, 25, 30. The target was 85, so the loop used all three rounds, and the paper it handed back is the draft, word for word. Both rewrites scored lower than the paper they replaced, so the best paper was the first, and the loop kept it. A team that cannot improve a paper should at least not make it worse, and that is a rule Python can keep.

Now look at what Python could not keep. The form asked for three sentences, and in round 1 it got three items: half a sentence, a blank, and the rest of the sentence. The count was right and the content was not. The researcher did find something a person could use. One note says that in two projects the developers took 31% more time when they wrote the tests first. No rewrite scored higher for having it.

🇹🇭 ไทย

ครั้งนั้นใช้เวลา 136 วินาทีบนแล็ปท็อป คะแนนเป็น 45, 25, 30 เป้าคือ 85 ลูปจึงใช้ครบทั้งสามรอบ และรายงานที่มันส่งกลับมาคือฉบับร่าง คำต่อคำ ฉบับที่เขียนใหม่ทั้งสองได้คะแนนต่ำกว่าฉบับที่มันมาแทน ฉบับที่ดีที่สุดจึงเป็นฉบับแรก และลูปเก็บฉบับนั้นไว้ ทีมที่ปรับปรุงรายงานไม่ได้ อย่างน้อยก็ไม่ควรทำให้มันแย่ลง และนั่นคือกฎที่ Python รักษาไว้ได้

ทีนี้ดูสิ่งที่ Python รักษาไว้ไม่ได้ แบบฟอร์มขอประโยคสามประโยค และในรอบที่ 1 มันได้มาสามรายการ: ครึ่งประโยค ช่องว่าง และส่วนที่เหลือของประโยค จำนวนถูกต้อง แต่เนื้อหาไม่ถูก นักค้นคว้าหาเจอสิ่งที่คนนำไปใช้ได้จริง โน้ตข้อหนึ่งบอกว่าในสองโปรเจกต์ นักพัฒนาใช้เวลามากขึ้น 31% เมื่อเขียนเทสต์ก่อน แต่ไม่มีฉบับเขียนใหม่ฉบับไหนได้คะแนนสูงขึ้นเพราะมีข้อนี้

🇨🇳 中文

这一次在笔记本上用了 136 秒。分数依次是 45、25、30。目标是 85,所以循环用完了三轮,而它交回的就是初稿,一字未改。两次重写的得分都比被它们替换的那一篇低,所以最好的是第一篇,循环留下的也是它。一个没法把报告改好的团队,至少不该把它改坏,而这是一条 Python 守得住的规矩。

再看 Python 守不住的。表格要的是三句话,第 1 轮它拿到了三项:半句话,一个空白,再加这句话的后半截。数目对了,内容却不对。研究员确实找到了人用得上的东西。有一条笔记说,在两个项目里,开发者先写测试时多花了 31% 的时间。可没有哪一次重写因为有了它而得分更高。

A SCORE IS NOT A CHECK The form guarantees that the score is a whole number from 1 to 100. It guarantees nothing about whether 45 is the right number. The reviewer is a small language model, and on an earlier try of ours, with looser instructions, the same model gave this draft a 7 and then, two rounds later, an 85. So use the score to decide when the loop stops, and read the paper yourself to decide whether it is good. Every review, every note and every draft is in research.db, saved the way the shop's orders were. And one run is one run: a web search finds other pages on another day, so your scores will differ from ours.แบบฟอร์มรับประกันว่าคะแนนเป็นจำนวนเต็มจาก 1 ถึง 100 แต่ไม่ได้รับประกันอะไรเลยว่า 45 เป็นตัวเลขที่ถูก ผู้รีวิวคือโมเดลภาษาขนาดเล็ก และในการลองครั้งก่อนหน้าของเรา ซึ่งคำสั่งหลวมกว่านี้ โมเดลตัวเดียวกันให้ฉบับร่างนี้ 7 คะแนน แล้วอีกสองรอบต่อมาก็ให้ 85 ดังนั้นให้ใช้คะแนนตัดสินว่าลูปจะหยุดเมื่อไร และอ่านรายงานด้วยตัวเองเพื่อตัดสินว่ามันดีหรือไม่ รีวิวทุกครั้ง โน้ตทุกข้อ และร่างทุกฉบับอยู่ใน research.db ซึ่งบันทึกไว้แบบเดียวกับรายการสั่งของร้าน และการรันหนึ่งครั้งก็คือการรันหนึ่งครั้ง: การค้นเว็บในวันอื่นจะเจอหน้าอื่น คะแนนของคุณจึงจะต่างจากของเรา表格能保证分数是一个 1 到 100 的整数。至于 45 是不是该给的那个数,它什么也保证不了。审稿人是一个小型语言模型,在我们更早的一次尝试里,指令写得更松,同一个模型给这篇初稿打了 7 分,两轮之后又打了 85 分。所以,用分数来决定循环何时停下,至于报告好不好,要你自己读了再判断。每一次审稿、每一条笔记、每一稿都在 research.db 里,保存的方式和店里的订单一样。还有,一次运行只是一次运行:换一天,网页搜索会找到别的页面,所以你的分数会和我们的不同。
YOUR TURN
  1. Change MODEL to qwen2.5:14b and run it again. Does the bigger model reach the target, and how long does it take?
  2. Write reviews.py, in the manner of runs.py. It opens research.db and prints the reviewer's runs, one line each.
  3. Write a short research paper of your own, about a choice your team has to make, and hand it to research.py.
  1. เปลี่ยน MODEL เป็น qwen2.5:14b แล้วรันอีกครั้ง โมเดลที่ใหญ่กว่าไปถึงเป้าหรือไม่ และใช้เวลานานเท่าไร
  2. เขียน reviews.py ในแบบเดียวกับ runs.py มันเปิด research.db แล้วพิมพ์การรันของผู้รีวิว บรรทัดละหนึ่งการรัน
  3. เขียนรายงานวิจัยสั้น ๆ ของคุณเอง เกี่ยวกับทางเลือกที่ทีมของคุณต้องตัดสินใจ แล้วส่งให้ research.py
  1. 把 MODEL 改成 qwen2.5:14b,再运行一次。更大的模型达到目标了吗?用了多久?
  2. 仿照 runs.py 写一个 reviews.py。它打开 research.db,把审稿人的运行逐行打印出来。
  3. 自己写一篇简短的研究报告,讲你的团队要做的一个选择,然后交给 research.py。