<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>from scratch</title>
    <link>https://imnewon.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 11 Jun 2026 23:20:21 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>imnewon</managingEditor>
    <item>
      <title>[논문 리뷰] SMORES-EP, a Modular Robot with Parallel Self-assembly</title>
      <link>https://imnewon.tistory.com/3</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2104.00800&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://arxiv.org/abs/2104.00800&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1780579517047&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;SMORES-EP, a Modular Robot with Parallel Self-assembly&quot; data-og-description=&quot;Self-assembly of modular robotic systems enables the construction of complex robotic configurations to adapt to different tasks. This paper presents a framework for SMORES types of modular robots to efficiently self-assemble into tree topologies. These mod&quot; data-og-host=&quot;arxiv.org&quot; data-og-source-url=&quot;https://arxiv.org/abs/2104.00800&quot; data-og-url=&quot;https://arxiv.org/abs/2104.00800v2&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b9JxCA/dJMb9cBPtPg/VfrlugKMRNBr7O7CrvcTj0/img.png?width=1200&amp;amp;height=700&amp;amp;face=0_0_1200_700,https://scrap.kakaocdn.net/dn/boeP3q/dJMb9frMSo8/6jtR2KcLCDHWOjqlEcmDZk/img.png?width=1000&amp;amp;height=1000&amp;amp;face=0_0_1000_1000&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2104.00800&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://arxiv.org/abs/2104.00800&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b9JxCA/dJMb9cBPtPg/VfrlugKMRNBr7O7CrvcTj0/img.png?width=1200&amp;amp;height=700&amp;amp;face=0_0_1200_700,https://scrap.kakaocdn.net/dn/boeP3q/dJMb9frMSo8/6jtR2KcLCDHWOjqlEcmDZk/img.png?width=1000&amp;amp;height=1000&amp;amp;face=0_0_1000_1000');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SMORES-EP, a Modular Robot with Parallel Self-assembly&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Self-assembly of modular robotic systems enables the construction of complex robotic configurations to adapt to different tasks. This paper presents a framework for SMORES types of modular robots to efficiently self-assemble into tree topologies. These mod&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;arxiv.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;Abstract&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;EP 자석을 사용하는 SMORES 형태의 Modular Robot 제어에 관한 논문입니다. 따라서 글에서는 하드웨어 구조나 실험에 대한 설명은 최소화하고, 논문의 핵심 기여점인 제어 알고리즘 부문을 중심으로 얘기해볼까 합니다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;생략된 내용이 다소 많으므로 참고용으로만 보시기를 바랍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;Introduction&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: justify;&quot;&gt;제한적 기능을 가진 단일 로봇(휴머노이드 등)에 비해,&lt;/span&gt;상황에 따라 조립과 분해가 가능한 모듈형 로봇(Modular Robot)은 무한한 잠재력을 가집니다. 복잡한 지형 극복, 물체 조작, 협력 이동 등 임무에 맞춰 로봇의 형태(Morphology)를 자유롭게 바꿀 수 있기 때문입니다. 그러나 모듈의 수가 늘어날수록 시스템의 제어 복잡도가 기하급수적으로 증가하고, 하드웨어적 제약 조건 하에서 충돌 없이 정밀하게 도킹(Docking)을 수행하는 것이 매우 어렵다는 한계가 있습니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;본 논문은 이러한 한계를 극복하고, 여러 개의 모듈형 로봇이 트리 토폴로지(Tree Topology) 형태로 효율적이고 안정적인 병렬 자가 조립(Parallel Self-assembly)을 수행할 수 있는 상위단 제어 알고리즘을 제안합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-04 at 10.37.01 PM.png&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crSRIX/dJMcaicx09e/NK2xQdBtMH4vhbg2touuZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crSRIX/dJMcaicx09e/NK2xQdBtMH4vhbg2touuZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crSRIX/dJMcaicx09e/NK2xQdBtMH4vhbg2touuZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrSRIX%2FdJMcaicx09e%2FNK2xQdBtMH4vhbg2touuZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;572&quot; height=&quot;380&quot; data-filename=&quot;Screenshot 2026-06-04 at 10.37.01 PM.png&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;806&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;SMORES-EP는 SMORES(Self-assembly MOdular Robot for Extreme Shape-shifting)의 최신 버전으로, 이름 그대로 극단적인 형태 변형을 목표로 설계된 모듈러 로봇입니다. 각&lt;span style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;10,1&quot;&gt;&lt;span&gt;&amp;nbsp;모듈은 컴팩트한 크기(80mm 정육면체)에 4 DoF(자유도)를 가집니다&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;10,2&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;10,3&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;10,3&quot;&gt;LEFT, RIGHT, PAN, TILT 축으로 구성되며, LEFT와 RIGHT 축은 바퀴 역할을 하여 차동 구동(Differential Drive) 방식으로 평면을 이동할 수 있습니다. LEFT, RIGHT, TOP, BOTTOM 총 4개의 면에 결합용 커넥터가 있습니다. 커넥터로 EP Magnet이라는 굉장히 흥미로운 자석이 활용되는데, &lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;10,3&quot;&gt;일반 전자석은 계속 전류를 흘려야 하지만 EP Magnet은 짧은 전류 펄스만으로 ON/OFF 상태를 변경할 수 있습니다. 따라서 전력 소모가 매우 적은데 결합력은 최대 약 90N에 달합니다. 논문에 따르면 약 4mm 정도 떨어져 있어도 자동으로 끌어당겨 결합이 가능하다고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;논문의&amp;nbsp;전체&amp;nbsp;흐름은&amp;nbsp;크게&amp;nbsp;세&amp;nbsp;단계로 나뉩니다.&lt;br /&gt;1. Root Module Search&lt;br /&gt;2. Task Assignment&lt;br /&gt;3. Docking Control&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;목표 topology가 주어지면,&lt;br /&gt;1. 루트 모듈을 찾고&lt;br /&gt;2. 각 실제 모듈에게 역할을 배정한 뒤&lt;br /&gt;3. 충돌 없이 이동하여 결합합니다.&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Modular Robot Topology Configuration&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;목표로 하는 그래프 G=(V, E)&lt;span style=&quot;color: #9d9d9d; text-align: justify;&quot;&gt;(G는&amp;nbsp;graph,&amp;nbsp;V는&amp;nbsp;노드,&amp;nbsp;E는&amp;nbsp;간선)&lt;/span&gt;가 주어지면, 알고리즘은 먼저 시스템의 중심이 될 루트 노드(&amp;tau;)를 찾습니다. 논문에서는 트리의 균형을 최적화하기 위해 트리 센트로이드(Tree Centroid) 개념을 활용합니다. 특정 노드를 기준으로 한 하위 서브트리들의 모듈 수가 전체 모듈 수의 과반수(1/2) 이하가 되도록 하는 노드를 &amp;tau;로 지정하는 방식입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-04 at 10.47.13 PM.png&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEDOKt/dJMcaayNjRr/siomJeKFjESv5014FaHqE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEDOKt/dJMcaayNjRr/siomJeKFjESv5014FaHqE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEDOKt/dJMcaayNjRr/siomJeKFjESv5014FaHqE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEDOKt%2FdJMcaayNjRr%2FsiomJeKFjESv5014FaHqE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;604&quot; height=&quot;138&quot; data-filename=&quot;Screenshot 2026-06-04 at 10.47.13 PM.png&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;tau; 기준 특정 방향에 결합된 모듈들이 전체 모듈의 과반수 이하일 경우 루트모듈임을 의미합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다음을 충족시키는 &lt;span style=&quot;text-align: start;&quot;&gt;&amp;tau;를 구하는 것이 topology configuration의 핵심이자 Algorithm 1의 내용이라고 볼 수 있겠습니다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;(1/4이 아닌 1/2인 이유에 대해 의문을 가졌었는데, 이는 tree controid 개념을 참고하시면 됩니다.)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-04 at 10.50.48 PM.png&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chwpUT/dJMcah5LO74/EWPzRnf7EywZCrNjdnpz4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chwpUT/dJMcah5LO74/EWPzRnf7EywZCrNjdnpz4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chwpUT/dJMcah5LO74/EWPzRnf7EywZCrNjdnpz4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchwpUT%2FdJMcah5LO74%2FEWPzRnf7EywZCrNjdnpz4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;518&quot; height=&quot;408&quot; data-filename=&quot;Screenshot 2026-06-04 at 10.50.48 PM.png&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;788&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;무작위 v_0을 구해서 트리를 루팅한 뒤,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt; 모든 h(v)가 1인 v(즉 leaf 위 노드)에 대해 자식 노드(leaf)의 CN을 합산(7열), &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;모든 h(v)가 1인 v의 부모 노드의 CN값을 구해 합산(8열)하여 상단 식을 충족시키는 v를 &amp;tau;로 지정합니다. &lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;해당 알고리즘은 Bottom-Up Dynamic Programming를 통해 O(|V|)의 시간복잡도로 계산될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;동시에 동시에 물리적 세계(평면 위)에서도 무게중심과 가장 가까운 최적의 실제 모듈(m_&amp;tau;​)을&amp;nbsp;선택해야&amp;nbsp;합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-04 at 11.09.24 PM.png&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceVSwe/dJMcabYMfbC/YAWr6PptRI2pNWtCK1kaNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceVSwe/dJMcabYMfbC/YAWr6PptRI2pNWtCK1kaNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceVSwe/dJMcabYMfbC/YAWr6PptRI2pNWtCK1kaNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceVSwe%2FdJMcabYMfbC%2FYAWr6PptRI2pNWtCK1kaNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;102&quot; data-filename=&quot;Screenshot 2026-06-04 at 11.09.24 PM.png&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;위 &lt;span style=&quot;color: #000000;&quot;&gt;식과 같이 전체 모듈들의 기하학적 중심(o_c)과 개별 모듈의 현재 위치(o_i) 사이의 유클리드 거리가 가장 최소인 모듈이 물리적 루트 모듈(m_&amp;tau;)이 됩니다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;m_&amp;tau;에 대한 모든 m_i의 상태는 rigid body transformation을 통해 표현됩니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Self-assembly Problem&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이제 실제로 결합할 차례입니다. &lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;결합의 목적은 SMORES-EP tree topology configuration G=(V,E) 를 실제로 형성하는 것입니다. 이때, &lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;평면 위에 펼쳐질 수 있는 topology만이 실제 결합될 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt; (아무래도 사방에 결합돼있는 구조는 구현하기가 곤란합니다.)&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 평면 위에 펼쳐질 수 있는 target kinematic topology G = (V,E)는 다음 조건을 만족합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;G is a connected graph;&lt;/li&gt;
&lt;li&gt;The Euclidean distance between two adjacent modules is w; &lt;span style=&quot;color: #9d9d9d;&quot;&gt;(w는 모듈의 길이)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;The center of every module occupies a unique location.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 우리가 해결해야 할 문제는, 충돌 없이 목표 topology를 형성하기 위한 결합 동작을 찾아내는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;&lt;b&gt;Task Assignment&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;루트&amp;nbsp;모듈이&amp;nbsp;결정되면&amp;nbsp;가상의&amp;nbsp;목표&amp;nbsp;토폴로지&amp;nbsp;상의&amp;nbsp;노드들과&amp;nbsp;평면&amp;nbsp;위에&amp;nbsp;흩어져&amp;nbsp;있는&amp;nbsp;실제&amp;nbsp;모듈들을&amp;nbsp;1:1로&amp;nbsp;매핑해&amp;nbsp;주어야&amp;nbsp;합니다.&amp;nbsp;본&amp;nbsp;논문에서는&amp;nbsp;모든&amp;nbsp;모듈의&amp;nbsp;총&amp;nbsp;이동&amp;nbsp;거리를&amp;nbsp;최소화하는&amp;nbsp;방향으로&amp;nbsp;최적화&amp;nbsp;문제를&amp;nbsp;정의하고,&amp;nbsp;이를&amp;nbsp;Kuhn-Munkres&amp;nbsp;(헝가리안)&amp;nbsp;알고리즘&amp;nbsp;등을&amp;nbsp;활용하여&amp;nbsp;해결합니다.&amp;nbsp;이&amp;nbsp;매핑&amp;nbsp;정보를&amp;nbsp;기반으로&amp;nbsp;각&amp;nbsp;모듈은&amp;nbsp;자신이&amp;nbsp;도달해야&amp;nbsp;할&amp;nbsp;최종&amp;nbsp;목표&amp;nbsp;위치와&amp;nbsp;결합해야&amp;nbsp;할&amp;nbsp;상대&amp;nbsp;모듈을&amp;nbsp;할당받게&amp;nbsp;됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #ffffff; text-align: start;&quot; data-copy-service-computed-style=&quot;font-family: sans-serif; font-size: 18.4px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom-width: 0px; border-bottom-style: none; border-bottom-color: rgb(230, 232, 240);&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-04 at 11.15.40 PM.png&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSOIsa/dJMcaaZPgEK/iFUPVtdwWj1FKqMPqyrUZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSOIsa/dJMcaaZPgEK/iFUPVtdwWj1FKqMPqyrUZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSOIsa/dJMcaaZPgEK/iFUPVtdwWj1FKqMPqyrUZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSOIsa%2FdJMcaaZPgEK%2FiFUPVtdwWj1FKqMPqyrUZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;601&quot; height=&quot;443&quot; data-filename=&quot;Screenshot 2026-06-04 at 11.15.40 PM.png&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;860&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #ffffff; text-align: start;&quot; data-copy-service-computed-style=&quot;font-family: sans-serif; font-size: 18.4px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom-width: 0px; border-bottom-style: none; border-bottom-color: rgb(230, 232, 240);&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #ffffff; text-align: left;&quot; data-copy-service-computed-style=&quot;font-family: sans-serif; font-size: 18.4px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom-width: 0px; border-bottom-style: none; border-bottom-color: rgb(230, 232, 240);&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Parallel Assembly Actions&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #ffffff; text-align: left;&quot; data-copy-service-computed-style=&quot;font-family: sans-serif; font-size: 18.4px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom-width: 0px; border-bottom-style: none; border-bottom-color: rgb(230, 232, 240);&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;매핑이 완료되면 루트 모듈을 중심으로 하는 그리드(Grid) 환경이 생성되며, Depth를 가진 모듈들이 병렬적으로 조립을 시작합니다. 도킹은 자가 조립에서 오차가 가장 발생하기 쉽고 실패 확률이 높은 단계이므로, 논문에서는 이를 Navigation &amp;rarr;Pose Adjustment &amp;rarr; Approach의 3단계 닫힌 루프 제어로 세분화했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #ffffff; text-align: left;&quot; data-copy-service-computed-style=&quot;font-family: sans-serif; font-size: 18.4px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom-width: 0px; border-bottom-style: none; border-bottom-color: rgb(230, 232, 240);&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Navigation: 모듈들은 다중 로봇 경로 플래너(Multi-vehicle Planner)를 통해 서로 충돌하지 않는 최적의 전역 경로를 생성한 뒤, 도킹 대상 모듈의 근처 그리드까지 이동합니다.&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-04 at 11.22.46 PM.png&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1EVlX/dJMcagsc7Ly/Z0hzKdCijilg32DbFtIHqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1EVlX/dJMcagsc7Ly/Z0hzKdCijilg32DbFtIHqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1EVlX/dJMcagsc7Ly/Z0hzKdCijilg32DbFtIHqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1EVlX%2FdJMcagsc7Ly%2FZ0hzKdCijilg32DbFtIHqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;308&quot; height=&quot;334&quot; data-filename=&quot;Screenshot 2026-06-04 at 11.22.46 PM.png&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #ffffff; text-align: left;&quot; data-copy-service-computed-style=&quot;font-family: sans-serif; font-size: 18.4px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom-width: 0px; border-bottom-style: none; border-bottom-color: rgb(230, 232, 240);&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Pose Adjustment: 도킹 직전 단계로, 타겟 커넥터면과 자신의 커넥터면을 완벽히 정렬하는 핵심 과정입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- TOP / BOTTOM Face 결합: 자체 DoF(자유도)를 활용하여 본인의 상태를 능동적으로 보정할 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- LEFT / RIGHT Face 결합: 모듈 구조상 좌우 면은 지면과 평행하게 구동되는 바퀴 축과 연결되어 있어, 평면 이동 상태에서는 자유로운 각도 정렬이 어렵습니다. 따라서 이 경우에는 추가적인 부속품을 장착한 별도의 Helping Module이 투입됩니다. &lt;span style=&quot;text-align: left;&quot;&gt;Helping Module&lt;/span&gt;이 도킹 대상 모듈을 위로 들어 올려(Lift) 공중에서 자유롭게 면을 정렬할 수 있도록 도운 후 목적지까지 배달해 줍니다.&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-04 at 11.29.21 PM.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXHOY9/dJMcai4zidt/yDjrdvFmUmX0ryw6aLvrWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXHOY9/dJMcai4zidt/yDjrdvFmUmX0ryw6aLvrWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXHOY9/dJMcai4zidt/yDjrdvFmUmX0ryw6aLvrWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXHOY9%2FdJMcai4zidt%2FyDjrdvFmUmX0ryw6aLvrWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;459&quot; height=&quot;338&quot; data-filename=&quot;Screenshot 2026-06-04 at 11.29.21 PM.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #ffffff; text-align: start;&quot; data-copy-service-computed-style=&quot;font-family: sans-serif; font-size: 18.4px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom-width: 0px; border-bottom-style: none; border-bottom-color: rgb(230, 232, 240);&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;Approach: 정렬이 완료되면 접근 후 결합합니다. 두 모듈의 거리가 영구전자석(EP)의 유효 거리(약 4mm) 이내로 들어오면 강한 자력에 의해 물리적으로 결합되며 도킹이 완료됩니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: center;&quot;&gt;이때, 평면 위의 루트 모듈은 바닥에 고정되어 있지 않기 때문에 모든 방향에서 동시에 도킹을 시도하면 위치가 틀어질 수 있습니다.&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt; 예외적으로 루트 모듈의 경우에는 LEFT Face와 RIGHT Face 결합을 우선적으로, TOP Face와 BOTTOM Face의 결합을 이후 진행합니다. 이러한 과정을 통해 결과적으로 주어진 topology를 구현해낼 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Experiments&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;논문서 제안한 프레임워크의 실효성을 검증하기 위해 세 가지 실험을 수행합니다.&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-05 at 12.01.33 AM.png&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;1204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1GT4L/dJMcacKalxn/ysmq1e6VkqznKG8B273pBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1GT4L/dJMcacKalxn/ysmq1e6VkqznKG8B273pBK/img.png&quot; data-alt=&quot;Task 1-Mobile Manipulator: 7개의 모듈이 조립되어 높은 곳의 물건을 집을 수 있는 형태를 구성합니다. (a)~(d)에서 병렬 조립을 보실 수 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1GT4L/dJMcacKalxn/ysmq1e6VkqznKG8B273pBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1GT4L%2FdJMcacKalxn%2Fysmq1e6VkqznKG8B273pBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1682&quot; height=&quot;1204&quot; data-filename=&quot;Screenshot 2026-06-05 at 12.01.33 AM.png&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;1204&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Task 1-Mobile Manipulator: 7개의 모듈이 조립되어 높은 곳의 물건을 집을 수 있는 형태를 구성합니다. (a)~(d)에서 병렬 조립을 보실 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-05 at 12.02.53 AM.png&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;1146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chkazD/dJMcad3g3iT/76EzCi2CgjpTAIVgLgii00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chkazD/dJMcad3g3iT/76EzCi2CgjpTAIVgLgii00/img.png&quot; data-alt=&quot;Task 2: Holonomic Vehicle: 9개의 모듈이 그리드 위에서 정밀한 타이밍으로 움직이며 충돌 없이 완벽한 이동 로봇 형태로 조립됩니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chkazD/dJMcad3g3iT/76EzCi2CgjpTAIVgLgii00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchkazD%2FdJMcad3g3iT%2F76EzCi2CgjpTAIVgLgii00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1766&quot; height=&quot;1146&quot; data-filename=&quot;Screenshot 2026-06-05 at 12.02.53 AM.png&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;1146&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Task 2: Holonomic Vehicle: 9개의 모듈이 그리드 위에서 정밀한 타이밍으로 움직이며 충돌 없이 완벽한 이동 로봇 형태로 조립됩니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-05 at 12.03.18 AM.png&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;854&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n1IvB/dJMcaffOL49/09U4CKNkjT7p4iKZYMtLK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n1IvB/dJMcaffOL49/09U4CKNkjT7p4iKZYMtLK1/img.png&quot; data-alt=&quot;Task 3: RC Car: 7개의 모듈이 결합하여 무거운 물체를 밀 수 있는 구조를 만듭니다. (b)에서 앞서 언급한 Helping Module이 투입되어 LEFT/RIGHT 면 도킹을 성공적으로 보조하는 모습을 확인할 수 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n1IvB/dJMcaffOL49/09U4CKNkjT7p4iKZYMtLK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn1IvB%2FdJMcaffOL49%2F09U4CKNkjT7p4iKZYMtLK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1770&quot; height=&quot;854&quot; data-filename=&quot;Screenshot 2026-06-05 at 12.03.18 AM.png&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;854&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Task 3: RC Car: 7개의 모듈이 결합하여 무거운 물체를 밀 수 있는 구조를 만듭니다. (b)에서 앞서 언급한 Helping Module이 투입되어 LEFT/RIGHT 면 도킹을 성공적으로 보조하는 모습을 확인할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-05 at 12.03.55 AM.png&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdRt8Y/dJMcagTiG46/IIS7PjLWR0MjxU42UmdW7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdRt8Y/dJMcagTiG46/IIS7PjLWR0MjxU42UmdW7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdRt8Y/dJMcagTiG46/IIS7PjLWR0MjxU42UmdW7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdRt8Y%2FdJMcagTiG46%2FIIS7PjLWR0MjxU42UmdW7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1294&quot; height=&quot;582&quot; data-filename=&quot;Screenshot 2026-06-05 at 12.03.55 AM.png&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-05 at 12.04.13 AM.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br0WCu/dJMcahq90bI/po5uIkT5trKIHdym26K6kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br0WCu/dJMcahq90bI/po5uIkT5trKIHdym26K6kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br0WCu/dJMcahq90bI/po5uIkT5trKIHdym26K6kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr0WCu%2FdJMcahq90bI%2Fpo5uIkT5trKIHdym26K6kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1238&quot; height=&quot;408&quot; data-filename=&quot;Screenshot 2026-06-05 at 12.04.13 AM.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;br /&gt;또한, 단순히 해체된 모듈들의 자가 조립뿐만 아니라, Walker(보행 로봇)형태에서 Mobile Manipulator 형태로 구조를 바꾸는 Self-reconfiguration 과정 역시 전체 분리 후 parallel assembly 알고리즘을 적용하여 시간을 단축시켰다고 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;Conclusion&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SMORES-EP 자체는 기존 연구의 연장선상에 가깝습니다. 오히려 핵심은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tree Centroid 기반 Root Selection&lt;/li&gt;
&lt;li&gt;Hungarian Algorithm 기반 Task Assignment&lt;/li&gt;
&lt;li&gt;Depth 기반 Parallel Assembly&lt;/li&gt;
&lt;li&gt;실제 하드웨어에서 동작 가능한 Docking Controller&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 하나의 파이프라인으로 통합했다는 점입니다. 물론 아직 VICON에 의존하고 있으며, 3차원 구조 생성이나 대규모 모듈 집단에 대한 검증은 부족합니다. 실제 야외 환경에서 사용하기까지는 갈 길이 멉니다. 그럼에도 불구하고 &quot;모듈형 로봇을 실제로 빠르게 조립시키려면 무엇이 필요한가?&quot;에 대한 상당히 현실적인 답을 제시한 논문이라고 생각합니다. 생략된 내용이 상당히 많으니 논문을 읽어보시기를 바랍니다.&lt;/p&gt;</description>
      <category>논문 리뷰</category>
      <author>imnewon</author>
      <guid isPermaLink="true">https://imnewon.tistory.com/3</guid>
      <comments>https://imnewon.tistory.com/3#entry3comment</comments>
      <pubDate>Fri, 5 Jun 2026 00:12:46 +0900</pubDate>
    </item>
    <item>
      <title>[공부] Opencode Tool-call Engine 리버스 엔지니어링</title>
      <link>https://imnewon.tistory.com/2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;AI Agent의 핵심 중 하나는 tool-call입니다. 저는 현재 SLM을 end-point로 바꿔도 잘 돌아가는 local agent를 만드는 것을 목표로 연구하고 있으며, 오늘은 tool-call에 대해 정리해보려 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 조립은 분해의 역순이라는 말을 정말 좋아합니다. tool call engine을 공부하기 위해, 실제 agent인 opencode의 tool call과 관련된 부분을 분석한 후 이것을 재조립해볼 생각입니다. 그리고 이 글은 '분해'에 해당합니다. 여러분들도 함께 opencode를 뜯어보시기를 바라겠습니다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;중간에 뭔가 잘못됐음을 깨달아 opencode랑 llm 디렉토리의 구분이 없습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/anomalyco/opencode&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/anomalyco/opencode&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1779401690380&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - anomalyco/opencode: The open source coding agent.&quot; data-og-description=&quot;The open source coding agent. Contribute to anomalyco/opencode development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/anomalyco/opencode&quot; data-og-url=&quot;https://github.com/anomalyco/opencode&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dOK8zq/dJMb9dHt1VJ/TCu5TwWfsRP02nFpP0xkfk/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/ndhd3/dJMb9c9DJvf/R9XlYbeMZ3dKsYFVH7mMpK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/bcfZsZ/dJMb9jOsLBB/8jGchkOkzY9WQ2F2AQSfz1/img.png?width=1824&amp;amp;height=1488&amp;amp;face=0_0_1824_1488&quot;&gt;&lt;a href=&quot;https://github.com/anomalyco/opencode&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/anomalyco/opencode&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dOK8zq/dJMb9dHt1VJ/TCu5TwWfsRP02nFpP0xkfk/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/ndhd3/dJMb9c9DJvf/R9XlYbeMZ3dKsYFVH7mMpK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/bcfZsZ/dJMb9jOsLBB/8jGchkOkzY9WQ2F2AQSfz1/img.png?width=1824&amp;amp;height=1488&amp;amp;face=0_0_1824_1488');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - anomalyco/opencode: The open source coding agent.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The open source coding agent. Contribute to anomalyco/opencode development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;opencode의 대략적인 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779392587005&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[진입점]
packages/opencode/src/index.ts  (yargs CLI)
         &amp;darr;
packages/opencode/src/cli/cmd/run.ts  (RunCommand)
         &amp;darr;
cli/cmd/run/runtime.ts  &amp;rarr;  runPromptQueue()  (직렬 큐)
         &amp;darr;
SDK client: session.prompt()
         &amp;darr;
[서버]
server/server.ts  &amp;rarr;  HTTP API 라우터
         &amp;darr;
[세션]
session/prompt.ts  &amp;rarr;  prompt()
         &amp;darr;
         loop()
         &amp;darr;
         runLoop()  &amp;larr; 여기가 메인 while(true) 루프&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래는 tool-call의 흐름입니다. AI SDK가 streamText()로 스트림 받으면서 tool call을 감지하고, resolveTools()에서 wrapping한 execute()가 호출되는 구조입니다. 루프는 툴 결과를 히스토리에 누적해서 계속 LLM을 재호출하는 방식으로 돌아갑니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779391519804&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;사용자 입력 (텍스트)
    &amp;darr;
[1] Session.prompt()           &amp;larr; 세션에 메시지 등록
    &amp;darr;
[2] resolveTools()             &amp;larr; 사용 가능한 도구 목록 수집
    &amp;darr;
[3] LLM.stream()               &amp;larr; AI에 요청 전송 (HTTP)
    &amp;darr;
[4] ToolRuntime.stream()       &amp;larr; 응답 스트리밍 + 도구 호출 감지
    &amp;darr;
[5] dispatch(tool, input)      &amp;larr; 도구 실행
    &amp;darr;
[6] 결과 주입 &amp;rarr; [3]으로 루프   &amp;larr; 결과를 다음 AI 요청에 포함&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 저희는 여기서 엔드포인트를 떼서 SLM에 갖다붙였을 때 동작하게 만들면 됩니다. 진입점과 서버, 기타 wrapping에 대해 전부 분석할 경우 머리가 터질테니 생략하고 tool call만 보겠습니다. 핵심은 &lt;i&gt;tool call이 어떻게 작동하는가&lt;/i&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 전에 구조부터 정리하고 가겠습니다. 현재 src를 하위에 두는 주요 패키지는 크게 두 개입니다. packages/llm packages/opencode&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;packages/llm은 AI SDK를 대체하기 위해 직접 구현 중인 자체 LLM 클라이언트 라이브러리입니다. Provider별 HTTP 프로토콜 처리, 스트리밍, tool execution 등을 직접 구현하고 있으며, 전체 구조는 Effect 기반으로 타입 안전하게 설계되어 있습니다. 또한&amp;nbsp;이&amp;nbsp;패키지는&amp;nbsp;앱&amp;nbsp;내부&amp;nbsp;전용&amp;nbsp;코드가&amp;nbsp;아니라,&amp;nbsp;외부에서도&amp;nbsp;@opencode-ai/llm&amp;nbsp;형태로&amp;nbsp;import해서&amp;nbsp;독립적으로&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;구성되어&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;반면 packages/opencode는 실제 애플리케이션 본체입니다. 현재 opencode는 아직 AI SDK 의 streamText()를 통해 LLM을 호출하고 있습니다. 여기서 중요한 포인트가 하나 있습니다. streamText()가&amp;nbsp;호출되는&amp;nbsp;순간부터&amp;nbsp;제어권이&amp;nbsp;AI&amp;nbsp;SDK&amp;nbsp;내부로&amp;nbsp;넘어가기&amp;nbsp;때문에,&amp;nbsp;이후&amp;nbsp;데이터가&amp;nbsp;어떤&amp;nbsp;경로로&amp;nbsp;흐르고&amp;nbsp;어떤&amp;nbsp;형태로&amp;nbsp;변환되는지를&amp;nbsp;외부에서&amp;nbsp;정확히&amp;nbsp;추적하기가&amp;nbsp;어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;따라서 이번 분석에서는 step 1~3까지는 packages/opencode, step 4~5부터는 packages/llm 기준으로 흐름을 따라갈 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;왜 분리되어 있냐? &amp;nbsp;packages/llm이 완성되면 packages/opencode의 AI SDK 의존성을 @opencode-ai/llm으로 교체할 계획인 것 같습니다. 지금은 마이그레이션 진행 중이라 둘이 공존하는 상태입니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STEP 1. 입력 처리(src/session/prompt.ts)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prompt.ts 코드는 opencode 프로젝트에서 AI 세션의 프롬프트 제어, Tool Call, Subtask 분기 등 핵심 오케스트레이션(Orchestration)을 담당하는 중추적인 백엔드 로직입니다. 사용자가 명령하면(코드가 PromptInput을 받으면) 아래 흐름으로 함수들이 호출됩니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779394311518&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export async function prompt(input: PromptInput) { 
  const userMessage = createUserMessage(input.text, input.attachments) 
  const tools = await resolveTools(input)
  await SessionProcessor.run(userMessage, tools)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;span style=&quot;caret-color: #9d9d9d;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;createUserMessage는 유저가&amp;nbsp;보낸&amp;nbsp;parts(텍스트,&amp;nbsp;파일,&amp;nbsp;에이전트&amp;nbsp;멘션&amp;nbsp;등)를&amp;nbsp;실제&amp;nbsp;메시지로&amp;nbsp;변환합니다.&amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MessageV2.User타입의 구조체를 만듭니다. 그냥 User의 Input을 받는 구조가 있군, 생각해두고 넘어갑니다. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이걸 어떻게 하지? 싶은 부분들은 대부분 TypeScript의 Effect 라이브러리가 해결해줍니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;span style=&quot;caret-color: #9d9d9d;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;* Effect 라이브러리? 타입 안전성과 에러 핸들링을 함수형 프로그래밍 스타일로 엮어주는 라이브러리&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STEP 2. ToolRegistry&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;(src/tool/registry.ts)&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;registry.ts에서는 AI가 사용할 수 있는 도구(tool)들을 모읍니다. 시스템 내부 registry에 등록된 툴들과, 외부 MCP 툴들을 파싱하고 현재 AI 모델이 이해할 수 있는 jsonSchema로 변환합니다. 이후 모델 종류와 설정(flag)에 따라 어떤 툴을 실제로 사용할 수 있을지 결정해 최종 Tool Registry를 구성합니다. (아래는 마찬가지로 요약 코드로, 실제와는 다릅니다.)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;span style=&quot;caret-color: #9d9d9d;&quot;&gt;* MCP(Model Context Protocol): 대규모 언어 모델(LLM)이 데이터베이스, 파일 시스템, 외부 API 등 다양한 외부 도구와 안전하게 연결될 수 있도록 돕는 AI-외부 시스템 연결 표준 프로토콜. 슬랙이나 노션 같은 거&lt;/span&gt;&lt;/span&gt;&lt;/i&gt;&lt;br /&gt;&lt;i&gt;&lt;span style=&quot;color: #9d9d9d; text-align: start;&quot;&gt;* Schema: AI 모델이 외부 도구(Tools), 데이터베이스, API와 상호작용하기 위한 '사용 설명서'이자 '데이터 규격'. ChatGPT API 생각해보면, 특정 틀에 맞춰 넣지 않으면 말귀를 못 알아듣습니다. 그 '특정 틀'이 schema에 해당합니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779397454521&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function resolveTools(input) {
  const tools = {}

  // 1. 내장 도구 등록
  tools[&quot;read&quot;]   = ReadTool      // 파일 읽기
  tools[&quot;write&quot;]  = WriteTool     // 파일 쓰기
  tools[&quot;shell&quot;]  = ShellTool     // 쉘 명령 실행
  tools[&quot;glob&quot;]   = GlobTool      // 파일 패턴 검색
  tools[&quot;grep&quot;]   = GrepTool      // 텍스트 검색

  // 2. 플러그인 도구 추가
  for (const plugin of loadedPlugins) {
    Object.assign(tools, plugin.tools)
  }

  // 3. MCP 도구 추가 (외부 서버 도구)
  const mcpTools = await loadMCPTools()
  Object.assign(tools, mcpTools)

  return tools
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tool이 생긴 모습은 src/tool.ts에서 볼 수 있습니다. 대충 아래처럼 생겼습니다. description이 매우 중요합니다. AI가 이 description을 읽고 &quot;어떤 도구를 언제 쓸지&quot;를 스스로 결정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779397503723&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Tool&amp;lt;T, S&amp;gt; {
  description: string          // AI에게 보여주는 설명
  parameters: Schema&amp;lt;T&amp;gt;        // 입력 타입 정의 (JSON Schema)
  success: Schema&amp;lt;S&amp;gt;           // 출력 타입 정의
  execute?: (params: T) =&amp;gt; Promise&amp;lt;S&amp;gt;  // 실제 실행 함수
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STEP 3. LLM 요청 전송&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;(src/session/llm.ts)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 선언과 정리를 하는 부분이고, 이제 슬슬 세션이 시작됩니다. 마찬가지로, 아래는 요약 코드입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779397620483&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function run(input: StreamInput) {
  const result = await streamText({
    model: languageModel,          // Anthropic / OpenAI / Gemini 등
    system: systemPrompt,          // AI의 역할 설명
    messages: conversationHistory, // 이전 대화 전체
    tools: toolDefinitions,        // 사용 가능한 도구 목록
    toolChoice: &quot;auto&quot;,            // AI가 알아서 도구 선택
    maxTokens: 8192,
  })
  
  return result.stream  // 스트림 반환 (토큰 하나씩 오는 것)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 AI의 응답을 한 번에 받는 것이 아니라, 토큰(단어 조각) 단위로 실시간으로 받습니다. 이게 스트리밍입니다. 터미널에서 AI가 타이핑하듯 출력되는 이유입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;opencode는 Anthropic, OpenAI, Google 등 다양한 provider를 지원합니다. 각 provider마다 API 형식이 다르기 때문에, 프로토콜 계층으로 추상화합니다 src/route/client.ts에 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779397768052&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LLMClient.stream()
    &amp;darr;
Route 선택 (모델 이름으로 매핑)
    &amp;darr;
Protocol.body.from() &amp;rarr; 제공자별 JSON 포맷 변환
    &amp;darr;
HTTP 요청 전송
    &amp;darr;
Protocol.stream.event() &amp;rarr; 제공자별 응답을 통일된 LLMEvent로 변환&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STEP 4. Tool-Call 감지 (src/tool-runtime.ts&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 핵심입니다. AI의 응답 스트림을 읽으면서 도구 호출 명령을 감지합니다. AI는 일반 텍스트 응답 대신 아래와 같은 구조화된 메시지를 반환합니다. 이게 tool call입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779397926395&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;tool_use&quot;,
  &quot;id&quot;: &quot;call_abc123&quot;,
  &quot;name&quot;: &quot;read&quot;,
  &quot;input&quot;: {
    &quot;path&quot;: &quot;/Users/kywn/opencode/README.md&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tool-runtime.ts는 아래처럼 굴러갑니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779397989865&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function* stream(llmStream, tools, stopWhen) {
  while (true) {
    const state = await accumulate(llmStream)  // 이벤트 수집
    
    yield state.events  // 텍스트, 추론 등 중간 이벤트 출력
    
    if (state.finishReason === &quot;tool_calls&quot;) {
      // 도구 호출이 있으면 병렬 실행
      const results = await Promise.all(
        state.toolCalls.map(call =&amp;gt; dispatch(tools, call))
      )
      
      // 결과를 다음 AI 요청에 포함
      llmStream = followUpRequest(state, results)
      
    } else {
      break  // 텍스트 응답이면 루프 종료
    }
    
    if (stopWhen(state)) break
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;accumulate()는 아래와 같은 이벤트들을 실행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779398033885&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;text-delta&quot;        &amp;rarr; 텍스트 조각 (화면에 실시간 출력)
&quot;reasoning-delta&quot;   &amp;rarr; 추론 과정 (Claude extended thinking)
&quot;tool-call&quot;         &amp;rarr; 도구 호출 요청 &amp;larr; 오늘의 핵심
&quot;finish-step&quot;       &amp;rarr; 이 단계 완료
&quot;finish&quot;            &amp;rarr; 전체 완료&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STEP 5. Tool 실행 (src/tool-runtime.ts&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 누가 불렀으면 대답을 해야겠죠... accmulate에서 tool-call이 감지되면 dispatch() 함수가 실행합니다. 이건 짧아서 진짜 코드 넣었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779398480685&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect&amp;lt;ToolResultValue&amp;gt; =&amp;gt; {
  const tool = tools[call.name]
  if (!tool) return Effect.succeed({ type: &quot;error&quot; as const, value: `Unknown tool: ${call.name}` })
  // tool이 아닌데 실행되면 곤란합니다.
  if (!tool.execute)
    return Effect.succeed({ type: &quot;error&quot; as const, value: `Tool has no execute handler: ${call.name}` })
    // tool이 빈손으로 나오면 아무래도 곤란하죠
  return decodeAndExecute(tool, call.input).pipe(
  // 실행합니다.
    Effect.catchTag(&quot;LLM.ToolFailure&quot;, (failure) =&amp;gt;
      Effect.succeed({ type: &quot;error&quot; as const, value: failure.message } satisfies ToolResultValue),
    ),
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779398561670&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const decodeAndExecute = (tool: AnyTool, input: unknown): Effect.Effect&amp;lt;ToolResultValue, ToolFailure&amp;gt; =&amp;gt;
  tool._decode(input).pipe(
    Effect.mapError((error) =&amp;gt; new ToolFailure({ message: `Invalid tool input: ${error.message}` })),
    // schema와 input이 다르면 곤란합니다.
    Effect.flatMap((decoded) =&amp;gt; tool.execute!(decoded)),
    // 아니면 실행이 됩니다.
    Effect.flatMap((value) =&amp;gt;
      tool._encode(value).pipe(
      // 출력 validation 검증하는 부분입니다.
        Effect.mapError(
          (error) =&amp;gt;
            new ToolFailure({
              message: `Tool returned an invalid value for its success schema: ${error.message}`,
            }),
        ),
      ),
    ),
    Effect.map((encoded): ToolResultValue =&amp;gt; ({ type: &quot;json&quot;, value: encoded })),
  )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 잡는 부분에 특별한 건 없고, 출력 validation도 검증하는 게 인상적입니다. 프롬프트 엔지니어링과 harness의 본질적인 차이는 '본인의 오류를 본인이 잡아낼 수 있는가'의 여부라는 말을 들었는데 이런 부분을 말씀하신 것 같습니다. (이걸 차치하고서라도 schema 맞추고 validation하는 게 tool call의 핵심입니다.) 여러모로 편리하지만 짜야할 사람 입장에서는 고려할 게 많다고 볼 수 있습니다. 특히 더 문제가 많은 SLM은 여러 번 확인하는 게 필수적입니다. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Effect가 다 해주는 것 같긴 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STEP 5. 루프 실행 (src/tool-runtime.ts&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tool-runtime.ts을 잘 살펴보시면 재귀인 걸 알 수 있습니다. (아무래도 루프니까요) 루프에 갇혀서 조건이 충족되기 전까지 계속 tool call을 한다고 보시면 되겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779399473619&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const loop = (request: LLMRequest, step: number): Stream.Stream&amp;lt;LLMEvent, LLMError&amp;gt; =&amp;gt;
    Stream.unwrap(
      Effect.gen(function* () {
        const state: StepState = { assistantContent: [], toolCalls: [], finishReason: undefined }

        const modelStream = options
          .stream(request)
          .pipe(Stream.tap((event) =&amp;gt; Effect.sync(() =&amp;gt; accumulate(state, event))))

        const continuation = Stream.unwrap(
          Effect.gen(function* () {
            if (state.finishReason !== &quot;tool-calls&quot; || state.toolCalls.length === 0) return Stream.empty
            .
            .
            .
            return resultStream.pipe(Stream.concat(loop(followUpRequest(request, state, dispatched), step + 1)))
          	// loop 안에 loop
          }),
          
        )

        return modelStream.pipe(Stream.concat(continuation))
      }),
    )
.
.
.
const followUpRequest = (
  request: LLMRequest,
  state: StepState,
  dispatched: ReadonlyArray&amp;lt;readonly [ToolCallPart, ToolResultValue]&amp;gt;,
) =&amp;gt;
  LLMRequest.update(request, {
    messages: [
      ...request.messages,
      Message.assistant(state.assistantContent),
      ...dispatched.map(([call, result]) =&amp;gt; Message.tool({ id: call.id, name: call.name, result })),
    ],
  })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;architecture적으로 보면 아래 같은 감성입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779399519185&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LLM stream
   &amp;darr;
tool-call 감지
   &amp;darr;
dispatch
   &amp;darr;
tool-result 생성
   &amp;darr;
followUpRequest
   &amp;darr;
다시 LLM 호출 (재귀)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한이나 보안 시스템은 스킵하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STEP 6. &amp;nbsp;SLM&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tool call의 주된 루프는 이 정도고, 코드 내에 흥미로운 부분이 있습니다. opencode 내부에 SLM 맞춤 코드가 존재합니다. src/session/llm.ts(line 134) small 플래그가 있으면 경량 설정을 적용한다든가,&lt;/p&gt;
&lt;pre id=&quot;code_1779399975503&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const base = input.small
        ? ProviderTransform.smallOptions(input.model)
        : ProviderTransform.options({
            model: input.model,
            sessionID: input.sessionID,
            providerOptions: item.options,
          })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src/provider/transform.ts(line 1186) SLM에서는 store: false로 response 저장을 안 하고, resoningEffort를 낮추거나 없애죠. gemini는 thinkingConfig라고 돼있는데 그냥 resoning과 같다 봐도 무방합니다. provider마다 이름이 다 다르지만 골자는 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779400204649&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function smallOptions(model: Provider.Model) {
  if (
    model.providerID === &quot;openai&quot; ||
    model.api.npm === &quot;@ai-sdk/openai&quot; ||
    model.api.npm === &quot;@ai-sdk/github-copilot&quot;
  ) {
    if (model.api.id.includes(&quot;gpt-5&quot;)) {
      if (model.api.id.includes(&quot;-chat&quot;)) {
        if (gpt5Version(model.api.id) === undefined) return { store: false }
        return { store: false, reasoningEffort: &quot;medium&quot; }
      }
      if (model.api.id.includes(&quot;search-api&quot;)) return { store: false }
      if (model.api.id.includes(&quot;5.&quot;) || model.api.id.includes(&quot;5-mini&quot;)) {
        return { store: false, reasoningEffort: &quot;low&quot; }
      }
      return { store: false, reasoningEffort: &quot;minimal&quot; }
    }
    return { store: false }
  }
  if (model.providerID === &quot;google&quot;) {
    // gemini-3 uses thinkingLevel, gemini-2.5 uses thinkingBudget
    return { thinkingConfig: googleSmallThinkingConfig(model.api.id) }
  }
  if (model.providerID === &quot;openrouter&quot; || model.providerID === &quot;llmgateway&quot;) {
    if (model.api.id.includes(&quot;google&quot;)) {
      return { reasoning: { enabled: false } }
    }
    return { reasoningEffort: &quot;minimal&quot; }
  }

  if (model.providerID === &quot;venice&quot;) {
    return { veniceParameters: { disableThinking: true } }
  }

  return {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Opencode의 코드를 통해 데이터가 흐르는 방향과 Tool-call이 움직이는 루프를 분해해 봤습니다. 굉장히 복잡하다고 생각했는데, 굉장히 복잡하다는 생각이 듭니다. 모든 코드에 대해 완전히 이해한 것은 아니지만 대략적인 흐름과 매커니즘을 이해한 데에 의의를 두기로 합니다. 틀린 내용이 있다면 지적해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대형 모델 대신 로컬 SLM을 엔드포인트로 쓸 때는 모델 체급이 작아지는 만큼 코드가 더 똑똑해져야 합니다. 툴 스키마 매칭 상태를 칼같이 감시하고, 컨텍스트 용량을 아끼기 위해 불필요한 Reasoning 설정을 깎아내는 처절한 최적화가 필요합니다.&amp;nbsp;뼈대를 완벽히 분해했으니, 다음 [조립편]에는(글을 쓸 수 있을지 모르겠으나,) 로컬 환경에서도 가볍고 기민하게 돌아가는 우리만의 'Local SLM Tool-Call Engine'을 직접 구현해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>공부</category>
      <author>imnewon</author>
      <guid isPermaLink="true">https://imnewon.tistory.com/2</guid>
      <comments>https://imnewon.tistory.com/2#entry2comment</comments>
      <pubDate>Fri, 22 May 2026 13:21:06 +0900</pubDate>
    </item>
  </channel>
</rss>