วิธีสร้าง Agent Harness ของคุณเอง

@mfpiccolo
อังกฤษ2 เดือนที่ผ่านมา · 28 พ.ค. 2569
339K
1.9K
242
35
4.9K

TL;DR

ผู้เขียนโต้แย้งว่าเฟรมเวิร์ก Agent แบบเดิมนั้นมีความแข็งตัวเกินไป จึงนำเสนอแนวทางแบบ Modular 'iii' engine ที่ส่วนประกอบทุกอย่างของ Harness ตั้งแต่ข้อมูลรับรองไปจนถึง Policy Engine จะเป็น Worker ที่เป็นอิสระและสามารถเปลี่ยนทดแทนกันได้

ทีมเอเยนต์ส่วนใหญ่ไม่ได้สร้างโครงสร้างพื้นฐานขึ้นมาเอง พวกเขาแค่เลือกใช้ตัวที่มีอยู่แล้ว LangChain, LangGraph, OpenAI Agents SDK, Anthropic SDK, CrewAI, AutoGen, ลูปเครื่องมือ, หน่วยความจำ, และการออเคสเตรชั่น — ทั้งหมดถูกหยิบมาจากชั้นวางเป็นแพ็กเกจเดียว โครงสร้างพื้นฐานคือเฟรมเวิร์กที่คุณ import เข้ามา ถ้ามีอะไรข้างในไม่เข้ากับคุณ คุณก็ fork มัน, สู้กับมัน, หรือหาทางอ้อม

Mike Piccolo - inline image

ผมคิดว่าการออกแบบแบบนั้นมันผิด และนั่นคือสาเหตุที่ทีมเอเยนต์ที่ทำงานมานานทุกทีมสุดท้ายก็ต้องเขียนโครงสร้างพื้นฐานของตัวเองขึ้นมาใหม่ตั้งแต่ต้น เพราะโครงสร้างพื้นฐาน它不是สิ่งเดียว มันเป็นสิบหรือสิบสองสิ่งที่แตกต่างกันถูกมัดรวมกัน เพราะระบบนิเวศรอบข้างไม่มีวิธีให้คุณประกอบมันเข้าด้วยกัน Pi agent กำลังมาในทิศทางที่ถูกต้อง แต่ก็ยังอยู่ในกระบวนทัศน์ของ "เพิ่มบริการอีกตัว แล้วรวมเข้ากับตัวอื่นทั้งหมด" เอ็นจินของ iii ปฏิบัติต่อเวิร์กเกอร์ทุกตัวเหมือนกัน และกำจัดตรรกะการรวมเข้ากันทั้งหมด เราเตอร์ของผู้ให้บริการ, คลังเก็บข้อมูลประจำตัว, เอ็นจินนโยบาย, ประตูอนุมัติ, แค็ตตาล็อกโมเดล, พื้นที่จัดเก็บเซสชัน, ตัวติดตามงบประมาณ, การกระจายฮุคหลังการเรียก, และลูปเทิร์นที่ทนทาน — สิ่งเหล่านี้คือข้อกังวลที่เป็นอิสระต่อกัน ทั้งหมดนี้ทำงานร่วมกับคิว, เซิร์ฟเวอร์ HTTP/API, การสตรีม, หรือแม้แต่เวิร์กเกอร์ในเบราว์เซอร์ของคุณได้ เฟรมเวิร์กที่ส่งมาพร้อมกันเป็นก้อนเดียวกำลังขายการแลกเปลี่ยนที่คุณไม่จำเป็นต้องทำ

แนวคิดเบื้องหลัง iii คือสิ่งเหล่านี้ไม่ควรรวมเป็นก้อนเดียวกัน ควรมีชุดของเวิร์กเกอร์ที่ทำงานบนเอ็นจินร่วมกัน แต่ละตัวสามารถแทนที่ได้, แต่ละตัวมีเวอร์ชันของตัวเอง, แต่ละตัวเชื่อมต่อกันด้วยพรีมิทีฟเดียว: ทริกเกอร์ (iii.trigger()) ที่เวิร์กเกอร์ตัวอื่น ๆ ก็ใช้เช่นกัน โครงสร้างพื้นฐานกลายเป็นสแต็กของเวิร์กเกอร์ที่ติดตั้งได้ และ "สร้างเอง" ไม่ได้หมายถึง "fork เฟรมเวิร์ก" อีกต่อไป หมายถึง "สลับเวิร์กเกอร์สองสามตัว"

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

15 หน้าที่ที่ระบบฮาร์เนสของเอเยนต์ต้องทำ

ถ้าคุณแยกส่วนระบบฮาร์เนสของเอเยนต์ในระดับโปรดักชันกลับไปเป็นความรับผิดชอบของมัน คุณจะได้รายการประมาณนี้:

  1. รับคำขอเทิร์นจากไคลเอนต์และบันทึกไว้
  2. แก้ไขข้อมูลประจำตัวสำหรับผู้ให้บริการโมเดลที่จะถูกเรียก
  3. ค้นหาว่าโมเดลที่เลือกทำอะไรได้บ้าง (วิทัศน์, เครื่องมือ, การสตรีม, หน้าต่างบริบท)
  4. ขับเคลื่อนสถานะเครื่องจักรต่อเทิร์น, จัดเตรียม, สตรีมผู้ช่วย, เรียกใช้เครื่องมือ, ควบคุม, และทำลาย
  5. โหลดและให้บริการสกิลบอดี้ที่อธิบายรูปร่างของคำขอของแต่ละฟังก์ชัน, รหัสข้อผิดพลาด, และหมายเหตุการใช้งาน
  6. ประกอบ system prompt, ย่อหน้าโหมด, คำนำหน้าเอกลักษณ์, ไดเรกทอรีการทำงาน, และภาคผนวกสกิลเริ่มต้น
  7. สตรีมโทเค็นกลับไปยังไคลเอนต์เมื่อโมเดลสร้างมันขึ้นมา
  8. ตรวจสอบทุกการเรียกใช้เครื่อง tool call (ซึ่งก็คือฟังก์ชันหนึ่ง) กับนโยบายก่อนที่จะรัน
  9. หยุดการเรียกใช้ tool call ที่ต้องการการตัดสินใจของมนุษย์ชั่วคราว และส่งคำตอบกลับไปยังเทิร์นที่ถูกต้อง
  10. ติดตามค่าใช้จ่ายของ LLM ตามงบประมาณต่อเวิร์กสเปซหรือต่อเอเยนต์
  11. รันฮุคก่อนและหลังการเรียกใช้ tool call (การบันทึก, การแก้ไข, เอฟเฟกต์ข้างเคียงแบบกำหนดเอง)
  12. บันทึกเซสชันเป็นแผนผังแบบแตกกิ่ง เพื่อให้การ fork และ resume ทำงานได้
  13. บีบอัดประวัติเซสชันเมื่อหน้าต่างบริบทเต็ม
  14. ปล่อยสตรีมเหตุการณ์ที่ UI สมัครสมาชิก
  15. ส่วนที่ขาดหายไปจากการสร้างบริษัทเอเยนต์ทุกแห่งที่ผมเห็น: ต้องมี OpenTelemetry trace หนึ่งตัวครอบคลุมทุกขั้นตอน เพื่อให้คุณดีบักมันได้

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

ระบบฮาร์เนสของ iii ส่งงานทั้งสิบสามอย่างนั้นเป็นเวิร์กเกอร์แยกกันบนทะเบียน workers.iii.dev แต่ละตัวใช้โปรโตคอล WebSocket เดียวกัน แต่ละตัวลงทะเบียนฟังก์ชันและทริกเกอร์บนบัสเอ็นจินเดียวกัน แต่ละตัวสามารถ iii worker add, สลับได้, และเขียนด้วยภาษาใดก็ได้ที่มี SDK

สแต็ก แยกตามเวิร์กเกอร์

นี่คือสแต็กโปรดักชันจริงจาก monorepo iii-hq/workers โดยแต่ละเวิร์กเกอร์มีหน้าที่ในหนึ่งบรรทัด ทั้งหมดอยู่ในแพ็กเกจที่ github.com/iii-hq/workers/harness:

Mike Piccolo - inline image

11 เวิร์กเกอร์ หนึ่งเอ็นจิน แต่ละตัวมีเวอร์ชันที่เผยแพร่แล้ว แต่ละตัวสามารถรันเป็นกระบวนการแยกกัน (pnpm dev:<worker> ใน dev, iii worker add <specific-worker> เป็นไบนารีที่เผยแพร่) หรือเป็นส่วนหนึ่งของจุดเริ่มต้นแบบผสมที่รันพวกมันพร้อมกัน

เหตุผลที่สิ่งนี้สำคัญ: ทุกกล่องในตารางนั้นคือจุดที่ใครบางคนสามารถส่งเวิร์กเกอร์ที่แตกต่างให้คุณได้ และคุณยังคงใช้ส่วนที่เหลือไว้ ไม่ชอบแค็ตตาล็อกโมเดลแบบสแตติก? เสียบเวิร์กเกอร์ที่ลงทะเบียน models::list และอ่านจาก API สด ไม่ชอบการเก็บข้อมูลประจำตัวแบบไฟล์? เสียบเวิร์กเกอร์ที่ลงทะเบียน auth::get_token และอ่านจากตัวจัดการความลับ ต้องการ FSM ของเทิร์นอื่นสำหรับเวิร์กโฟลว์ที่แตกกิ่งต่างกัน? แทนที่ turn-orchestrator ทุกตัวที่พึ่งพามันเรียก run::start และอ่าน turn_state ผ่านบัสเดียวกัน ดังนั้นสแต็กที่เหลือไม่เปลี่ยนแปลง

วิธิการทำงานของลูปจริง ๆ

รูปร่างของหนึ่งเทิร์นเป็นแบบนี้ โดยไล่ตามลำดับการทำงานของเวิร์กเกอร์

เบราว์เซอร์/CLI/แชท POST เทิร์นผ่าน harness::trigger ด้วย {session_id, message_id, payload} เวิร์กเกอร์ harness meta-worker ส่งต่อ payload ไปยัง run::start การ hop นี้มีไว้เพื่อให้ตัวห่อ OpenTelemetry span สามารถกำหนด seed ให้กับ session ID และ message ID เป็น baggage ซึ่งจะแพร่กระจายไปยังทุกการเรียก iii.trigger ที่ซ้อนกันในทุกเวิร์กเกอร์ในสแต็ก ต้นไม้ trace อีกด้านหนึ่งเป็นกราฟที่เชื่อมต่อกันเป็นหนึ่งเดียว

run::start ไปถึง turn-orchestrator มันคงคำขอรัน, กำหนดค่าเริ่มต้นของ TurnStateRecord ใน iii state ที่ session/<sid>/turn_state และส่งคืนทันที งานจริงเกิดขึ้นภายในเครื่องสถานะที่ทนทานต่อเทิร์น ซึ่งถูกปลุกโดยการ publish ไปยัง FIFO ของ turn-step

สถานะปลายทางสองสถานะคือ stopped (ออกอย่างสะอาดผ่าน finishSession()) และ failed (การโยนข้อผิดพลาดจาก handler ที่ไม่คาดคิดจะมาจบที่นี่, ack คิวเพื่อให้มันหยุดลองใหม่, และแสดง message_complete{stop_reason:'error'} พร้อม agent_end เพื่อให้ UI แสดงเหตุผล) การ teardown เป็น finishSession() inline ที่เรียกจากเส้นทางจบเทิร์นใด ๆ ไม่ใช่ขั้นตอนที่ถูก enqueue แยกต่างหาก

provisioning ทำสามสิ่ง มันบูต microVM iii-sandbox ถ้าการรันต้องการการทำงานแบบแยกเดี่ยว มันเรียก directory::skills::download สำหรับทุก namespace ใน system_default_skills (ค่าเริ่มต้น ["iii://iii-directory/index"]) เพื่อให้ iii-directory แคชสกิลบอดี้ที่การรันเริ่มต้นด้วยไว้ล่วงหน้า และมันประกอบ system prompt เป็นสามชั้น: ย่อหน้าโหมดที่เลือกจาก run_request.mode (plan, ask, หรือ agent), คำนำหน้าเอกลักษณ์ของ iii ที่สอนโมเดลเกี่ยวกับแบบแผน agent_trigger และรูปแบบการค้นหาแบบ on-demand ของ directory::skills::get, และดัชนีที่ผนวกของสกิลเริ่มต้นที่เอเยนต์บูตด้วย ผู้เรียกสามารถแทนที่พรอมพ์ทั้งหมดได้โดยส่ง system_prompt บน run::start; มิฉะนั้น orchestrator จะสร้างมันขึ้นมา สคีมาฟังก์ชันมาจากแค็ตตาล็อกเอ็นจินที่ใช้งานจริง

assistant_streaming เรียก provider::<name>::stream บนเวิร์กเกอร์ผู้ให้บริการที่ตรงกับฟิลด์ provider ของรัน เวิร์กเกอร์ผู้ให้บริการดึงข้อมูลประจำตัวผ่าน auth::get_token (auth-credentials), สตรีมการตอบสนอง SSE ของโมเดลไปยังช่อง iii, และ orchestrator ระบายช่องนั้นโดยส่งเหตุการณ์ message_update บน agent::events สำหรับการกระจายไปยัง UI การสร้างช่องและลูปการอ่านอยู่เบื้องหลัง MessagePump แบบ pull-based ใน provider-stream.ts ดังนั้นสถานะการสตรีมยังคงจดจ่ออยู่กับการเปลี่ยนสถานะ

เมื่อผู้ช่วยส่งคืน tool call, FSM จะเข้าสู่ function_execute ทุก tool call ผ่าน dispatchWithHook ซึ่งเป็นจุดคอขวดเดียวใน orchestrator consultBefore เรียก policy::check_permissions โดยตรงโดยมี timeout 5 วินาที เวิร์กเกอร์นโยบาย (harness meta-worker ในสแต็กเริ่มต้น) อ่าน iii-permissions.yaml, จับคู่ function_id ของการเรียกกับชุดกฎ และส่งคืนหนึ่งในสามผลลัพธ์:

  • allow: dispatch ดำเนินต่อไป; orchestrator ทริกเกอร์ฟังก์ชันเป้าหมายและเขียนผลลัพธ์
  • deny: dispatch ถูกตัดจบด้วย DenialEnvelope ผลลัพธ์กลายเป็นบันทึกการปฏิเสธ
  • needs_approval: การเรียกนั้นถูกจอดไว้ในลิสต์ awaiting_approval ของเทิร์น ส่วนที่เหลือของ batch ยังคง dispatch ต่อไป เทิร์นจะเปลี่ยนเป็น function_awaiting_approval เมื่อมีรายการที่รออยู่หนึ่งรายการขึ้นไปเท่านั้น

การปลุกการอนุมัติเป็นแบบ reactive และใช้ร่วมกัน orchestrator ลงทะเบียนทริกเกอร์สถานะ turn::on_approval เพียงตัวเดียวบนขอบเขต approvals เมื่อคอนโซลเรียก approval::resolve, approval-gate worker เขียน approvals/<sid>/<cid> = {decision, reason} ไปยัง iii state การเขียนนั้นจะยิง turn::on_approval ซึ่งทำให้เซสชันที่เกี่ยวข้องคืบหน้า function_awaiting_approval อ่านเฉพาะการตัดสินใจที่เพิ่งมาถึง, dispatch แต่ละอันเมื่อมันมาถึง (allow กลายเป็นการ dispatch ที่ได้รับการอนุมัติล่วงหน้า, deny หรือ aborted กลายเป็นการปฏิเสธสังเคราะห์), และคืบหน้าเมื่อ awaiting_approval[] ว่างเปล่า ไม่มีฟังก์ชัน resume ต่อการเรียกให้ลงทะเบียน ไม่ต้องสแกนใหม่เมื่อเริ่มต้นเพื่อกู้คืนการอนุมัติที่ค้างอยู่ ทริกเกอร์ตัวเดียวครอบคลุมทุกเซสชัน

Fail-closed โดยโครงสร้าง: ถ้าเวิร์กเกอร์นโยบายไม่สามารถเข้าถึงได้หรือ timeout 5 วินาทีเกิดขึ้น consultBefore จะปฏิเสธการเรียกด้วยซองจดหมาย gate_unavailable ถ้า iii::durable::publish เองเกิดข้อผิดพลาด, การกระจายฮุคจะส่งคืน publish_failed: true และ orchestrator จะถือว่ามันเป็นการปฏิเสธ

มีข้อดีด้านเวลาแฝงหลายอย่างที่เกิดขึ้นจากรูปร่างนี้ ฮุคหลังการเรียกฟังก์ชันจะลัดวงจร publish_collect ผ่านแคชการมีอยู่ของผู้สมัครสมาชิกเมื่อไม่มี subscriber แบบ durable ลงทะเบียนสำหรับทอปิกนั้น ซึ่งช่วยลดประมาณ 500ms ต่อการเรียกใช้ฟังก์ชันที่ดำเนินการ tearing_down ถูก inline ไว้ใน finishSession() ซึ่งลดการ hop ของ durable queue หนึ่งครั้งต่อเทิร์น context-compaction สมัครสมาชิกสตรีม agent::turn_end โดยเฉพาะที่ orchestrator ปล่อยที่ขอบเขตของเทิร์น ดังนั้นการปลุก compactor จึงเป็นต่อเทิร์นแทนที่จะเป็นต่อเหตุการณ์ ทริกเกอร์สถานะ session-create fanout เกตตามขอบเขตเท่านั้นและจับคู่ในกระบวนการ ดังนั้น RPC harness::session::is_create_event ก่อนหน้านี้ต่อการเขียนจึงหายไป

หลังจาก batch เสร็จสมบูรณ์ steering_check ตัดสินใจว่าจะดำเนินการต่อ, หยุด, หรือถึง max_turns ถ้าดำเนินการต่อ, วนกลับไปที่ assistant_streaming ถ้าหยุดหรือ max_turns, finishSession() ทำงานแบบ inline: ปล่อย agent_end, ปลดปล่อย sandbox, เปลี่ยนเป็น stopped

ตลอดการรันทั้งหมด, ทุกเวิร์กเกอร์ที่เข้าร่วมจะปล่อย OTel spans ที่ติดแท็กด้วย iii.session.id, iii.message.id, และ iii.function.id แท็กเหล่านั้นคือสิ่งที่ engine::traces::group_by ของเอ็นจินอ่านเพื่อเติม "Group by Session" / "Group by Message" / "Group by Function" ใน UI traces การ instrumentation เป็นแบบอัตโนมัติ: src/runtime/worker.ts ห่อทุก registerFunction ด้วย Proxy ดังนั้นไม่มีโค้ดต่อเวิร์กเกอร์ใดต้องจำเพิ่ม spans

สร้างเอง

ส่วนที่น่าสนใจคือไม่มีเวิร์กเกอร์ใดข้างต้นที่พิเศษ แต่ละตัวเป็นโพรเซสที่เปิด WebSocket ไปยังเอ็นจิน ลงทะเบียนฟังก์ชันและทริกเกอร์บางอย่าง แล้วรัน สัญญาเหมือนกับสัญญาที่เวิร์กเกอร์แอปพลิเคชันทุกตัวใช้ ระบบฮาร์เนสสร้างขึ้นบนพรีมิทีฟเดียวกันกับตรรกะทางธุรกิจของคุณ

ซึ่งหมายความว่า "สร้างระบบฮาร์เนสของคุณเอง" แยกย่อยเป็นการดำเนินการเดียวกับ "เขียนเวิร์กเกอร์ใด ๆ" คุณเลือกเลเยอร์ที่ต้องการแทนที่, คุณเขียนเวิร์กเกอร์ที่ลงทะเบียนฟังก์ชันเดียวกันบนบัส, คุณ iii worker add มัน, และสแต็กที่เหลือเริ่มใช้เวิร์กเกอร์ของคุณ

สองเลเยอร์ที่ไม่ปรากฏในตารางเวิร์กเกอร์ด้านบน แต่มีความสำคัญต่อพฤติกรรมของระบบฮาร์เนส สกิลคือวิธีที่เวิร์กเกอร์แต่ละตัวโฆษณาว่าฟังก์ชันของมันทำอะไร เวิร์กเกอร์ทุกตัวสามารถเผยแพร่สกิลที่ iii://<worker>/<function> ซึ่งเอเยนต์จะเรียกผ่าน directory::skills::get ก่อนเรียกฟังก์ชันนั้นเป็นครั้งแรก system prompt จะถูกประกอบต่อเทิร์นจากย่อหน้าโหมด, คำนำหน้าเอกลักษณ์ของ iii, และสกิลบอดี้เริ่มต้นที่การรันถูกกำหนดค่าให้ใช้ ทั้งสองอย่างขับเคลื่อนด้วยบัส: สกิลให้บริการโดยเวิร์กเกอร์ iii-directory, system prompt ประกอบโดย turn-orchestrator ทั้งสองอย่างสามารถแทนที่ได้

ห้าตัวอย่างที่เป็นรูปธรรม

แทนที่แค็ตตาล็อกโมเดลด้วย API สด เขียนเวิร์กเกอร์ที่ลงทะเบียน models::list, models::get, models::supports ให้มันดึงข้อมูลจากเอนด์พอยต์แค็ตตาล็อกของผู้ให้บริการของคุณทุก ๆ N นาทีและแคช เผยแพร่มัน iii worker add your-org/dynamic-models-catalog หยุดเวิร์กเกอร์ models-catalog แบบสแตติก turn-orchestrator ไม่รู้ว่ามีความแตกต่าง มันเรียก iii.trigger('models::list') และเอ็นจินจะเส้นทางไปยังเวิร์กเกอร์ที่ลงทะเบียนฟังก์ชันไอดีนั้นล่าสุด

เพิ่มผู้ให้บริการใหม่ รูปร่างของ provider-kimi และ provider-lmstudio พิสูจน์ให้เห็นแล้ว แต่ละตัวคือเวิร์กเกอร์ตัวเดียวที่ลงทะเบียน provider::<name>::stream และ provider::<name>::complete, ระบายสตรีม SSE จาก API ต้นทางไปยังช่อง iii, และเขียนการใช้งานโมเดลไปยัง llm-budget ผ่าน budget::record การเพิ่มผู้ให้บริการที่ห้าคือการเขียนโฟลเดอร์เดียวที่มี iii.worker.yaml หนึ่งอันและ register.ts หนึ่งอัน เผยแพร่ไปยัง registry หรือเก็บไว้ในเครื่อง turn-orchestrator เลือกผู้ให้บริการตามฟิลด์ provider ของรัน; ผู้ให้บริการใหม่จะพร้อมใช้งานทันทีที่เวิร์กเกอร์เชื่อมต่อ

ให้บริการสกิลจากที่เก็บอาร์ติแฟกต์ส่วนตัว เขียนเวิร์กเกอร์ที่ลงทะเบียน directory::skills::get และ directory::skills::list ซึ่ง backed โดยระบบเอกสารภายในของคุณหรือ S3 bucket ส่วนตัว ตัดการเชื่อมต่อหรือเปลี่ยนชื่อเวิร์กเกอร์ iii-directory เริ่มต้น การบูตสแตรปของ orchestrator เรียก directory::skills::download ต่อ namespace; เวิร์กเกอร์ของคุณตอบกลับ รูปแบบ "เรียกสกิลต่อฟังก์ชันก่อนเรียกฟังก์ชันใหม่" ของเอเยนต์ยังคงทำงานไม่เปลี่ยนแปลงเพราะรูปร่างของ wire เดิม

แทนที่ system prompt ทั้งหมด run::start รองรับฟิลด์ system_prompt ที่เป็น optional ส่งมันแล้ว orchestrator จะใช้สตริงของคุณตามตัวอักษร, ข้ามการประกอบย่อหน้าโหมด + คำนำหน้าเอกลักษณ์ + ภาคผนวกสกิล มีประโยชน์เมื่อคุณมีทรัพย์สินพรอมพ์ที่มีอยู่แล้วที่คุณต้องการให้ระบบฮาร์เนสให้เกียรติโดยไม่ต้องแก้ไข การดาวน์โหลดสกิลยังคงทำงานในการบูตสแตรป ดังนั้นเอเยนต์ยังคงมีความสามารถในการค้นหา directory::skills::get แบบ on-demand แม้จะมีพรอมพ์แบบกำหนดเอง

แทนที่พื้นผิว UI ของประตูอนุมัติ เวิร์กเกอร์ approval-gate เริ่มต้นลงทะเบียน approval::resolve สคีมา wire คือการเรียกฟังก์ชันเดียว:

handler จะคง approvals/<sid>/<cid> = {decision, reason} ไว้ใน iii state ทริกเกอร์สถานะ turn::on_approval เพียงตัวเดียวของ orchestrator จะรับการเขียนนั้นและปลุกเซสชันที่ถูกต้อง ถ้าคุณต้องการขับเคลื่อนการอนุมัติจาก Slack แทนคอนโซล, เขียนเวิร์กเกอร์ Slack ที่ฟังคำสั่ง slash command /approve <id> และ /deny <id>, แล้วเรียก approval::resolve ด้วย payload ที่ถูกต้อง orchestrator ไม่รู้ว่ามีความแตกต่าง เวิร์กเกอร์ approval-gate ทั้งหมดยังคงไม่ถูกแตะต้อง คุณเพิ่มเวิร์กเกอร์ใหม่; คุณไม่ได้แทนที่เวิร์กเกอร์ที่มีอยู่

ถ้าคุณต้องการเอ็นจินนโยบายอื่น (OPA, Cedar, DSL ของคุณเอง), เขียนเวิร์กเกอร์ที่ลงทะเบียน policy::check_permissions และส่งคืน { decision, rule_id?, matched_constraint? } ตัดการเชื่อมต่อเวิร์กเกอร์นโยบายเริ่มต้น (ซึ่งถูกห่อไว้ใน harness meta-worker, ดังนั้นคุณจะปิดการใช้งาน handler นั้นหรือรัน meta-worker ที่ลดขนาดลง) consultBefore ของ turn-orchestrator ไม่รู้ว่ามีความแตกต่าง timeout 5 วินาทีเดียวกัน, ความหมาย fail-closed เดียวกัน, รูปร่าง wire เดียวกัน

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

ระบบฮาร์เนสคือแถบเลื่อน ไม่ใช่ทางแยก

การถกเถียงเกี่ยวกับระบบฮาร์เนสแบบคลาสสิกมักตั้งกรอบเป็นแบบบางกับแบบหนา ลูปแบบบางของ Anthropic เทียบกับ DAG แบบชัดแจ้งของ LangGraph กรอบนี้สมมติว่าคุณเลือกข้างใดข้างหนึ่งแล้วอยู่กับมัน

เมื่อระบบฮาร์เนสประกอบด้วยเวิร์กเกอร์บนบัสเดียวกัน, แบบบางกับแบบหนาก็แค่จำนวนเวิร์กเกอร์ที่คุณติดตั้ง ระบบฮาร์เนสแบบบางคือ turn-orchestrator บวก provider-anthropic บวก auth-credentials บวก harness meta-worker ขนาดเล็กที่สุด เท่านั้นเอง ไม่มีการอนุมัติ, ไม่มีงบประมาณ, ไม่มีเอ็นจินนโยบาย, ไม่มีการกระจายฮุค รันอะไรก็ได้ ไว้ใจโมเดล มีประโยชน์สำหรับเอเยนต์วิจัยอิสระ, ลูปทดลอง, หรืออะไรก็ตามที่อยู่ภายใน

ระบบฮาร์เนสแบบหนาคือเวิร์กเกอร์ทั้งหมดสิบสามตัว บวก context-compaction บวกเวิร์กเกอร์นโยบายแบบกำหนดเอง บวก approval-gate แบบกำหนดเอง บวกพื้นผิวการอนุมัติที่รวมกับ Slack บวกเวิร์กเกอร์งบประมาณที่บังคับใช้ขีดจำกัดต่อเวิร์กสเปซ มีประโยชน์สำหรับเอเยนต์ที่ทำงานเวิร์กโฟลว์ของลูกค้าที่ทุก tool call ต้องตรวจสอบได้ และค่าใช้จ่ายโมเดลทุกบาทต้องรวมไปยังแดชบอร์ดการเงิน

ระยะห่างทางสถาปัตยกรรมระหว่างแบบบางและแบบหนาไม่ใช่การเขียนใหม่ มันคือการเปลี่ยน config โปรโตคอล wire เดียวกัน, รูปร่าง trace เดียวกัน, เรื่องการสังเกตการณ์เดียวกัน แถบเลื่อนเลื่อนโดยการเพิ่มและลบเวิร์กเกอร์ออกจาก config.yaml ของคุณ ทุกอย่างอื่นคงที่

มันใช้ได้ภายในเวิร์กเกอร์ตัวเดียวด้วย turn-orchestrator เพิ่งปล่อยรีแฟกเตอร์ที่ยุบ FSM จากสิบเอ็ดสถานะเหลือเจ็ด, ลบกลไก turn::approval_resume::<sid>/<cid> ต่อการเรียกเพื่อใช้ทริกเกอร์สถานะ turn::on_approval แบบ reactive เพียงตัวเดียวบนขอบเขต approvals, และ inline tearing_down เป็น finishSession() port เวิร์กเกอร์อื่นทุกตัวในสแต็ก (approval-gate, session, llm-budget, providers, models-catalog, auth-credentials, hook-fanout, context-compaction) ยังคงไม่เปลี่ยนแปลง รูปร่าง wire ของ approval::resolve ไม่ขยับ สัญญาคงอยู่ นั่นคือคุณสมบัติที่องค์ประกอบนี้มอบให้: การเขียนภายในครั้งใหญ่ของเวิร์กเกอร์ตัวเดียวคือการเปลี่ยนแปลงที่อยู่ในตัวเอง เพราะเพื่อนบ้านทุกตัวสื่อสารกับมันผ่านฟังก์ชันไอดีระดับบัส

นี่คือส่วนที่โมเดลเฟรมเวิร์กไม่สามารถให้คุณได้ เฟรมเวิร์กเลือกตำแหน่งบนแถบเลื่อนให้คุณแล้วล็อคคุณไว้ โมเดลเวิร์กเกอร์ปล่อยแถบเลื่อนไว้ในมือของคุณ

สิ่งนี้หมายถึงอะไรในทางปฏิบัติ

ถ้าคุณรันเอเยนต์บนเฟรมเวิร์กและรู้สึกถึงปัญหาเรื่องขอบเขตเดียวกันที่ทีมส่วนใหญ่เจอเมื่อขยายขนาด คำตอบน่าจะไม่ใช่ "เขียนระบบฮาร์เนสใหม่ในเฟรมเวิร์กของเราเอง" เอ็นจินนโยบายขยายไม่ได้ตามที่คุณต้องการ UI อนุมัติถูกเชื่อมเข้ากับพื้นผิวแชทของเฟรมเวิร์ก ที่เก็บข้อมูลประจำตัวไม่สามารถคุยกับตัวจัดการความลับของคุณได้ ตัวติดตามงบประมาณอยู่ในฐานข้อมูล sidecar ที่ trace มองไม่เห็น คำตอบคือเปลี่ยนไปใช้ซับสเตรตที่ระบบฮาร์เนสถูกแยกส่วนตั้งแต่แรก

วิธีที่เร็วที่สุดที่จะรู้สึกถึงข้อโต้แย้งนี้คือ clone github.com/iii-hq/workers, pnpm install, pnpm build, และรันจุดเริ่มต้นแบบผสม คุณจะได้ระบบฮาร์เนสสิบสี่เวิร์กเกอร์เต็มรูปแบบที่ชี้ไปยัง iii engine คุณสามารถปิดการใช้งานเวิร์กเกอร์ใดก็ได้โดยลบรายการออกจากลิสต์บูต คุณสามารถสลับเวิร์กเกอร์ใดก็ได้โดยเขียนตัวแทนที่ลงทะเบียนฟังก์ชันไอดีเดียวกัน คุณสามารถขยายเวิร์กเกอร์ใดก็ได้โดยเพิ่ม subscriber ไปยังทอปิกฮุคของมัน hook-fanout::publish_collect เป็น generic ที่ฮุค iii ทุกตัวสร้างขึ้น

เอกสารอยู่ที่ iii.dev/docs เอ็นจินอยู่ที่ github.com/iii-hq/iii ทะเบียนเวิร์กเกอร์อยู่ที่ workers.iii.dev ชุดรวมระบบฮาร์เนสอยู่ที่ github.com/iii-hq/workers/harness

แนวคิด

ระบบฮาร์เนสไม่ใช่สิ่งที่คุณติดตั้ง ระบบฮาร์เนสคือชุดของงานที่ระบบของคุณต้องทำเพื่อให้เอเยนต์ทำงานได้อย่างทนทาน, ปลอดภัย, และสังเกตได้ ยุคของเฟรมเวิร์กมัดรวมงานเหล่านั้นเข้าด้วยกันเพราะไม่มีอะไรด้านล่างให้วิธีประกอบมัน

แนวคิดของ iii คือพรีมิทีฟเดียว: เวิร์กเกอร์ที่เชื่อมต่อกับเอ็นจินผ่าน WebSocket และลงทะเบียนฟังก์ชันและทริกเกอร์ มีขนาดเล็กพอที่จะรองรับทุกงานเหล่านั้นแยกกัน, และสแต็กที่ได้จะมีประโยชน์มากกว่าเฟรมเวิร์กใด ๆ เพราะทุกเลเยอร์สามารถแทนที่ได้อย่างอิสระ

คุณไม่ใช่ "adopt" ระบบฮาร์เนสของ iii คุณติดตั้งเวิร์กเกอร์ที่คุณต้องการ, เขียนเวิร์กเกอร์ที่คุณต้องการ, และจบลงด้วยระบบฮาร์เนสที่มีรูปร่างตรงกับระบบของคุณทุกประการ โปรโตคอลเดียวกันในทุกเลเยอร์ trace เดียวกันในทุกการเรียก iii worker add เดียวกันสำหรับส่วนที่คุณนำมาจาก registry และสำหรับส่วนที่คุณเผยแพร่เอง

นี่คือหน้าตาของ "สร้างระบบฮาร์เนสของเอเยนต์ของคุณเอง" เมื่อซับสเตรตมีรูปร่างที่ถูกต้อง เลือกเวิร์กเกอร์ เขียนเวิร์กเกอร์ที่ขาดหายไป ประกอบ ระบบฮาร์เนสคือการประกอบ

ร่วมสร้างระบบฮาร์เนสของเอเยนต์ที่สมบูรณ์แบบที่โลกยุคใหม่ต้องการ: discord.gg/iiidev

iii เป็นโอเพนซอร์ส เริ่มต้นได้ที่ iii.dev/docs เวิร์กเกอร์ระบบฮาร์เนสอยู่ที่ github.com/iii-hq/workers และเอ็นจินอยู่ที่ github.com/iii-hq/iii

— Mike Piccolo, ผู้ก่อตั้งและ CEO @iiidevs

บันทึกในคลิกเดียว

อ่านบทความไวรัลเชิงลึกด้วย AI ใน YouMind

บันทึกแหล่งที่มา ถามคำถามที่ตรงประเด็น สรุปข้อโต้แย้ง และเปลี่ยนบทความไวรัลให้เป็นโน้ตที่นำกลับมาใช้ได้ใน AI เวิร์กสเปซเดียว

สำรวจ YouMind
สำหรับครีเอเตอร์

เปลี่ยน Markdown ของคุณให้เป็นบทความ 𝕏 ที่สะอาดตา

เวลาคุณเผยแพร่งานเขียนยาวของตัวเอง การจัดรูปแบบรูปภาพ ตาราง และบล็อกโค้ดให้เข้ากับ 𝕏 นั้นน่าปวดหัว YouMind เปลี่ยนร่าง Markdown ทั้งฉบับให้เป็นบทความ 𝕏 ที่สะอาดตาและพร้อมโพสต์ทันที

ลอง Markdown เป็น 𝕏

แพตเทิร์นให้ถอดรหัสเพิ่มเติม

บทความไวรัลล่าสุด

สำรวจบทความไวรัลเพิ่มเติม