AI 에이전트가 선박 일지를 작성할 때, 누가 무엇을 작성했는지 기록하는 법
요약
AI 에이전트, 인간, 자동화 시스템이 혼재된 선박 로그북에서 데이터의 출처(provenance)를 명확히 구분하기 위한 설계 방안을 다룹니다. signalk-logbook에 origin 필드를 추가하여 작성 주체를 명시하고 EU의 AI 라벨링 권고를 준수하는 방법을 설명합니다.
핵심 포인트
- 로그북 내 데이터 출처(manual, auto, agent) 명시를 위한 origin 필드 도입
- AI 에이전트 작성 항목에 대한 EU 권장 AI 라벨 부착
- 기존 데이터의 작성자 관례 유도를 통한 레거시 항목 처리
- 데이터 모델의 무결성을 유지하기 위한 author 필드 오버로딩 방지
요약 (TL;DR) — 인간, 선박 자동화 시스템, 그리고 AI 에이전트가 모두 동일한 로그북(logbook)을 작성할 때, 항목별 출처(provenance) 정보가 필요합니다. 우리는 signalk-logbook (PR #88, 0.11.0 버전에서 출시됨)의 상위 단계에 선택 사항인 origin: manual | auto | agent 필드를 추가했습니다. 또한 기존 항목(legacy entries)의 경우 소비자가 작성자 관례(author conventions)로부터 이를 유도할 수 있도록 했으며, 에이전트가 작성한 줄에는 EU에서 권장하는 AI 라벨을 부착했습니다. 설계 부분으로 바로 이동하세요.
우리 선박의 일지에는 세 종류의 작성자가 있습니다. 한 명의 인간이 한 줄을 타이핑(또는 구두로 입력)합니다. 로그북 플러그인 자체가 매시간 항해 로그, 알림 상태 변경, 승무원 교체와 같은 무인 항목(unattended entries)을 작성합니다. 그리고 우리의 음성 에이전트가 REST API를 통해 "이 순간을 기록해줘", 훈련 요약, 구조 신호 수신 등의 항목을 작성합니다. 이 세 가지 모두 동일한 일일 YAML 파일에 기록됩니다.
이 포스트가 답하는 질문은 다음과 같습니다: 6개월 후, 과거의 기록을 읽을 때 어떤 줄을 실제로 인간이 작성했는지 어떻게 알 수 있을까요?
문제 (Problem)
다음은 이 작업을 수행하기 전 우리 아카이브에 있던 일일 파일입니다. 세 개의 항목, 세 명의 서로 다른 작성자가 있지만, 유일한 출처 정보는 우연에 의한 것입니다:
- datetime: '2026-06-11T08:01:00.000Z'
text: 'Motoring at 6.1kt'
author: '' # 플러그인 자체의 시간별 작성자
...
작성자(author)가 비어 있다는 것은 플러그인의 무인 작성자가 우연히 작성자 없이 stateToEntry(state, text)를 호출하기 때문에 "플러그인이 작성했다"는 것을 의미할 뿐입니다. 에이전트 항목이 hermes라고 표시되는 이유는 단지 MCP 서버가 보유한 인증 토큰(authentication token)의 이름이 그것이기 때문입니다. 데이터 모델 어디에도 _이 줄은 AI에 의해 작성되었다_라고 명시되어 있지 않습니다. 구절을 요약하는 에이전트, 아침 브리핑, 일지를 검토하는 인간 등 독자들은 인간이 작성한 줄과 기계의 출력을 신뢰할 수 있는 방식으로 구분할 수 없습니다.
과거에는 이것이 있으면 좋은(nice-to-have) 기능이었습니다. 하지만 이제는 그렇지 않습니다. 에이전트가 작성한 기록에는 레이블링 (labeling)이 필요합니다. EU는 AI 생성 콘텐츠 레이블링을 위한 권장 아이콘을 발표했으며, 로그북 (logbook)은 신뢰와 수정을 위해 "누가 이것을 작성했는가"가 매우 중요한 기록의 전형적인 사례입니다.
우리가 거부한 설계들
author 필드 과부하. 출처 (provenance) 정보를 author 문자열에 쑤셔 넣는 것(author: "bryan (via agent)")은 아카이브 (archive)가 이미 가지고 있는 유일하고 깨끗한 신호를 파괴하며, 이를 사용하는 모든 소비자(consumer)가 별도의 파서 (parser)를 만들어야 하게 만듭니다.
구조화된 source 객체. source: {kind: agent, model: …, version: …} 방식은 아무도 요청하지 않은 유연하기만 한 버전입니다. 하나의 열거형 (enum)이 실제 질문에 답할 수 있습니다. 중첩된 객체 (nested object)는 영원히 유지 관리해야 하는 스키마 (schema) 표면을 늘릴 뿐입니다.
화자에게 자신을 식별하도록 요청하기. 음성 입력의 경우, 모든 로그 라인 앞에 "누구십니까?"라고 묻는 것은 인체공학 (ergonomics) 측면에서 최악이며, 우리의 STT 스택 (Whisper)은 어차피 화자 인식 (speaker recognition) 기능이 없습니다. 심문하는 것보다는 가정하고 확인하는 방식이 더 낫습니다 (자세한 내용은 아래 참조).
설계
하나의 선택적 열거형 (enum), 그리고 그것이 의미하는 바에 대한 명확한 규칙:
author= 책임자.origin= 문장을 구성한 주체.
이는 종이 로그북의 당직자 (watchkeeper) 모델입니다. 계기판이 수치를 읽었더라도 당직 사관이 페이지에 서명하는 것과 같습니다. 책임 (responsibility)과 저작 (authorship)은 서로 다른 열 (column)입니다.
manual— 사람이 텍스트를 구성함. 타이핑 또는 구두 전달: 음성으로 구두 전달된 라인은manual입니다. 음성 파이프라인 (pipeline)은 단지 펜의 역할을 할 뿐이기 때문입니다.agent— AI가 텍스트를 구성함 (훈련 요약,mark_moment문구 등).auto— 무인 기계 (시간별 로그, 알림 트리거).
구두 전달 (dictation) 지점이 미묘한 부분입니다. 만약 _전송 방식 (transport)_에 따라 분류한다면 (API 작성 = 기계), 모든 음성 입력이 기계 출력물이 되어 레이블의 의미가 퇴색됩니다. _누가 단어를 선택했는가_에 따라 분류하십시오.
하위 호환성을 고려한 배포
해당 아카이브에는 72개의 항목이 있었지만 명시적인 출처(provenance) 정보는 전혀 없었습니다. 우리는 파일을 마이그레이션(migrate)하는 것을 거부했고, 상위(upstream) 릴리스가 나올 때까지 기다리며 작업을 중단하고 싶지도 않았습니다. 그래서 배포는 양방향으로 진행되었습니다.
- 작성자가 이제
origin을 찍습니다. 우리의 MCP 서버는 상위 시스템이 해당 필드의 존재를 알기도 전에 POST 바디(body)에origin: "agent"를 보내기 시작했습니다. 서버의 필드 화이트리스트(whitelist)가 이를 조용히 누락시켰기에, 해롭지 않은 무작정 동작(no-op)이었습니다. 상위 릴리스가 보트 서버에 적용된 날, 이 스탬프(stamps)들이 효과를 발휘하기 시작했습니다. 별도의 조정은 필요하지 않았습니다. - 소비자(Consumers)는 기존 항목에 대해 아카이브가 이미 인코딩하고 있는 작성자 관례(author conventions)로부터
origin을 유도합니다. 명시적인 필드가 있으면 그것이 우선하며, 과거의 데이터는 휴리스틱(heuristics)으로 채웁니다.
Upstream: PR #88
PR #88 (머지됨, signalk-logbook 0.11.0에서 릴리스됨)은 엔드 투 엔드(end to end)로 필드를 추가합니다. 핵심은 하나의 파라미터(parameter)입니다:
// plugin/format.js
module.exports = function stateToEntry(state, text, author = '', origin = 'manual') {
const data = {
...
사람의 개입이 없는 작성자들 — 알림 항목, 트리거 항목(오토파일럿, 선원, 돛 변경), 매시간 작성되는 항해 로그 — 는 origin: 'auto'를 전달합니다. POST /logs는 바디(body)에서 author와 origin을 모두 허용합니다:
// plugin/entryFields.js
if (typeof body.author === 'string' && body.author.length >= 1) {
// 위임(Delegation): 인증된 클라이언트(음성 비서, 선원 앱)가 작성
...
author 측면은 origin 측면만큼이나 중요합니다. 이는 인증된 클라이언트가 누군가를 대신하여 한 줄을 작성하고 그들을 대신해 서명할 수 있게 해줍니다. PUT은 이미 바디에서 제공된 작성자를 준수하고 있었으며, 이로써 POST도 일관성을 갖게 되었습니다. 유효하지 않은 origin은 에러를 발생시키는 대신 무시됩니다. 선택적인 출처(provenance) 필드 때문에 로그 항목이 거부되어서는 안 되기 때문입니다.
메인테이너(maintainer)와의 리뷰를 통해 두 가지 개선 사항이 도출되었습니다:
기존 항목은 읽기 시점에 확정적인 origin을 갖게 됩니다. 모든 소비자(consumer)가 "필드 없음" 상태를 처리하도록 만드는 대신, 플러그인은 읽을 때 기본값을 설정합니다. 이는 읽기 시점의 category 기본값과 동일한 패턴입니다:
// plugin/Log.js — 저장된 일일 파일 읽기
origin: entry.origin || (entry.author ? 'manual' : 'auto'),
저장된 origin(기원)은 보존됩니다. 작성자(author)가 없는 레거시 항목은 auto로 읽히며(이는 플러그인 자체의 기록이었습니다), 작성자가 있는 항목은 manual로 읽힙니다. 이를 사용하는 소비자(Consumers)는 항상 확정된 값을 보게 됩니다.
에이전트 항목은 UI에서 EU AI 라벨을 받습니다. 유지 관리자는 EU의 AI 생성 콘텐츠 라벨링 아이콘을 지목했습니다. 이 배지는 로그북 테이블, 타임라인, 항목 뷰어의 agent 항목 내 작성자 옆에 표시됩니다:
// src/components/OriginBadge.jsx
function OriginBadge(props) {
if (props.origin !== 'agent') {
...
툴팁 텍스트는 의도된 것입니다. EU 자체 테스트 결과, 아이콘 단독 사용보다 아이콘과 텍스트 라벨을 함께 사용하는 것이 더 효과적임이 밝혀졌습니다. manual 및 auto 항목은 수정 없이 렌더링됩니다. 즉, 이 라벨은 AI 출력물을 표시하는 것이지, 모든 것을 장식하는 것이 아닙니다.
동반 PR인 #92는 책임(responsibility) 측면을 완성합니다. 이 PR은 기존의 crewNames(선원 이름) 옆에 모든 항목에 communication.skipperName(선장 이름)을 스냅샷으로 찍어 저장하며, 선장 교대(skipper handoffs)를 자동으로 기록합니다. 작성권(origin), 책임(author), 지휘(skipperName)가 각각 고유한 필드에 담겨 있으므로, 로그에서 직접 해상 근무 기록을 위한 선장 대 선원 근무 일수와 같은 정보를 도출할 수 있습니다.
소비자 측면: MCP 서버에서 origin 도출하기
logbook-mcp는 우리 에이전트들이 사용하는 도구 인터페이스입니다. 이 도구의 읽기 경로(read path)는 명시적 필드를 우선하고, 그다음 작성자 관례를 따르는 기존의 휴리스틱(heuristic)을 구현합니다:
def derive_origin(
entry: dict,
agent_authors: frozenset[str] = DEFAULT_AGENT_AUTHORS, # {"hermes", "poseidon"}
...
주요 목록들은 배포 사실(deployment facts)이기 때문에 설정(configuration)에 해당합니다. 즉, 당신의 에이전트 토큰에는 당신의 이름이 담겨 있습니다. read_entries에 의해 반환되는 모든 항목은 파생된 origin을 포함하며, 이 도구는 origin 필터 기능을 갖추어 "화요일의 인간(human) 항목만 보여줘"라는 요청을 단 한 번의 호출로 처리할 수 있습니다.
쓰기 경로(write path)는 수동(manual) 대 에이전트(agent) 규칙에 따라 구성(composition)별로 스탬프를 찍습니다:
# mark_moment: 기본값 origin="agent" — 에이전트가 문구를 구성함.
# 구두로 전달된 라인은 origin="manual"을 통과함 — 인간이 이를 구성함.
await client.post_entry(text, category=category, origin=origin)
그리고 signalk-distress-core — 수신된 구조 신호(distress traffic)를 로그북에 기록하는 DSC 및 AIS-distress 플러그인의 기반이 되는 공유 라이브러리 — 는 auto를 찍습니다:
// lib/logbook.js — DSC 수신은 플러그인 자동 반응(plugin-automatic reaction)이며,
// 에이전트의 추론(agent reasoning)이 아님.
// origin-field PR이 배포되기 전까지는 signalk-logbook에 의해 무시되며,
// 배포 후에는 명시적으로 처리됨.
...
마지막 분류는 잠시 짚고 넘어갈 가치가 있습니다. 구조 신호 작성기(distress writer)는 "AI 스택" 내부에서 실행되지만, 모델이 해당 라인을 구성하는 것이 아니라 파서(parser)가 수행합니다. origin은 시스템의 분위기(vibe)가 아니라 구성자(composer)를 설명합니다.
음성 귀속(Voice attribution): 가정, 확인, 수정
POST에서의 author 위임은 사용자가 실제로 체감하는 부분을 가능하게 했습니다. 누군가 로그 항목을 구두로 전달할 때, 스택에는 화자 ID(speaker ID)가 없습니다. 따라서 에이전트는 작성자(당직자; 현재는 단독 항해를 기본값으로 가정)를 _가정(assumes)_하고 그 가정을 말로 내뱉습니다. mark_moment의 반환 값은 귀속 정보로 끝나는, 바로 말할 수 있는 형태의 확인 문구입니다:
Logged. Entry 4. 14:32. 48.76°N 123.2°W. Logged as Bryan.
만약 가정이 틀렸다면, 화자는 그냥 그렇게 말하면 되며, 수정 경로(correction path)는 별도의 도구로 존재합니다:
async def amend_entry_author(client, entry_id: str, author: str) -> dict:
"""기존 항목의 작성자를 재귀속함 — 음성 수정 경로."""
# 날짜를 가져오고, 항목을 찾아, 수정된 작성자와 함께 다시 PUT 함
...
"아니, 그건 Sarah였어" → amend_entry_author → "수정되었습니다. 항목이 이제 Sarah로 기록되었습니다." 가정 후 확인(Assume-and-confirm) 방식은 한 번의 발화 절(spoken clause)만 소모합니다. 매 항목마다 화자에게 질문하는 방식은 이 기능의 효용성을 떨어뜨릴 것입니다.
한 가지 구현상의 까다로운 점: 이 기능을 출시할 당시에는 상위 POST 요청에서 아직 author를 설정할 수 없었기 때문에(오직 PUT만 이를 준수함), mark_moment는 항목을 POST한 다음 본문에서 제공된 작성자와 함께 다시 PUT합니다. 이 2단계 방식은 구형 서버에서도 여전히 작동하며, 이는 원본 타임스탬프(origin stamps)와 동일한 전방 호환성(forward-compatibility) 태도를 유지합니다.
중요성 / 주의사항
- 작성 시점에 기록하지 않은 출처(Provenance)는 영원히 재구성해야 하는 출처가 됩니다. 우리의 72개 항목 아카이브는 작성자 휴리스틱(heuristics)으로 복구할 수 있을 만큼 충분히 작습니다. 토큰 이름이 순환되는 10,000개의 항목 규모라면 불가능할 것입니다. 작성자는 향후 기록에 스탬프를 찍으며, 휴리스틱은 엄격하게 과거를 위해서만 존재합니다.
- 전송 방식(transport)이 아닌 작성자(composer)에 따라 분류하세요. API로 작성되었다고 해서 반드시 기계가 작성(dictation, 받아쓰기)한 것은 아니며, AI 스택 내부에서 실행된다고 해서 반드시 AI가 작성(AI-composed)한 것도 아닙니다(DSC 파서의 경우). 이를 잘못 분류하면 AI 레이블은 노이즈가 됩니다.
- 누락된 선택적 필드(optional fields)는 단 한 곳에서, 읽기 시점(read time)에 기본값을 가져야 합니다. 읽기 시점의 기본값(
entry.origin || (entry.author ? 'manual' : 'auto'))은 어떤 소비자(consumer)도 "필드 누락"을 기준으로 분기 처리하지 않음을 의미합니다. 플러그인 내부에서는 비용이 저렴하지만, 다른 모든 곳에서는 비용이 비싸집니다. - 스키마가 확정되기 전에 작성자(writers) 기능을 먼저 출시하세요. 서버의 화이트리스트(whitelist)에서 제외되는 필드는 무료 전방 호환성 베팅과 같습니다. 오늘날에는 무해하지만, 상위 릴리스가 배포되는 날 별도의 동기화된 업그레이드 없이도 효과를 발휘합니다.
- 에이전트 출력물에 레이블을 붙이는 것은 이제 기본 요건(table stakes)이 되고 있습니다. EU는 정확히 이 용도로 기성 아이콘들을 발표하고 있으며, 이를 취미 수준의 해양 플러그인에 연결하는 데는 약 30줄 정도가 소요되었습니다. 만약 당신의 에이전트가 나중에 인간이 신뢰할 수 있는 기록을 작성한다면, 이를 수행하지 않을 변명은 거의 없습니다.
우리는 모든 것이 전기 구동되는 차터 카타마란 (charter catamaran) 프로젝트의 배후에 있는 boat-agent 시스템에서 이 스택을 실행합니다. 선박 일지 (ship's log)는 선박에 있으며, 이제 누가 기록을 작성했는지 알려줍니다. MCP 측 코드는 sailingnaturali/logbook-mcp에 있습니다.
관련 내용: 애초에 로그북이 어떻게 SignalK 서버에 올라가게 되었는지 — Adopt vs build: why we deleted our working logbook for SignalK.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기