6.6 KiB
title, category, date, excerpt, preview-cover
| title | category | date | excerpt | preview-cover | ||||
|---|---|---|---|---|---|---|---|---|
| DevLog @ 2026.01.01 | DevLog | 2026-01-01 | AIRI 의 iOS 플랫폼 진전과 그 과정에서 만난 문제·해결책, 그리고 LemonNeko 가 FlowChat 에서 진행한 기억 계층 실험의 성과와 구현 세부 사항을 나눕니다. |
|
::: info AI 번역 이 글은 중국어 원문을 AI 로 영어로 옮긴 판을 다시 한국어로 번역한 것입니다. 중국어 원문은 여기에서 볼 수 있습니다. 번역에 문제가 있다면 편하게 이슈를 열거나 Pull Request 를 보내 주세요. :::
새해 복 많이 받으세요! AIRI 메인테이너 중 한 명인 @LemonNekoGH 입니다. 새해 첫 DevLog 는 제 차례네요. (B 키를 눌러 웃는 이모티콘 선택) 하하하하하!
자, 본론으로 갑시다.
AIRI Pocket
이틀 전, AIRI 의 모바일 애플리케이션을 만들기 위해 Capacitor 를 도입했습니다 (#845). 이를 AIRI Pocket 이라고 부릅니다.
iOS 를 동작시켰고 알림 기능도 추가했습니다. 즉 그녀가 원한다면 알림을 통해 함께 시간을 보내자고 먼저 말을 걸 수 있습니다.
기본 Capacitor 아이콘은 너무 신경 쓰지 마세요. 나중에 교체할 예정입니다.
영상에서 저는 AIRI 를 백그라운드 앱 목록에서 제거했고, 잠시 뒤 AIRI 가 알림을 띄웠습니다. 이런 백그라운드 알림은 PWA 에서는 구현하기 어렵지만 네이티브 iOS 앱에서는 아주 쉽습니다.
잠깐, 그렇게 순조로웠을까요? 문제가 없었을까요?
안전하지 않은 컨텍스트로 인한 기능 제약
당연히 문제가 있었습니다. 첫 번째는 VAD(음성 활성 감지) 컴포넌트였습니다. VAD 는 AudioWorkletNode 에 의존하는데, 이 클래스는 보안 컨텍스트에서만 쓸 수 있습니다. 그런데 Capacitor 의 iOS 앱은 개발 중 핫 리로드가 필요해서 개발 환경이 노출한 포트에 직접 접근합니다. 그 결과 브라우저가 이를 안전하지 않은 컨텍스트로 판단해 AudioWorkletNode 클래스를 제공하지 않고, VAD 가 실패합니다.
패키징 후 프로덕션에서는 보안 컨텍스트가 되지만 개발 중에도 테스트해야 하니 이 문제는 반드시 풀어야 했습니다.
AI 와 검색 엔진의 도움으로 vite-plugin-mkcert 플러그인을 찾았습니다. 자체 서명 인증서를 생성해 시스템에 설치해 주어 브라우저가 보안 컨텍스트로 인식하게 만들어 줍니다.
그래서 해결됐을까요? 아직입니다. 인증서가 로컬 시스템에는 설치됐지만 iOS 에는 설치되지 않아서 WKWebView 가 이 인증서를 신뢰하지 않습니다. 그런데 IP 가 바뀔 때마다 인증서를 다시 설치해야 한다면 너무 번거롭습니다.
개발 중에는 네이티브 코드를 직접 고쳐서 모든 인증서를 신뢰하게 하면 어떨까요? 실제로 동작합니다:
import UIKit
import Capacitor
import WebKit
class DevBridgeViewController: CAPBridgeViewController {
#if DEBUG
override func viewDidLoad() {
super.viewDidLoad()
bridge?.webView?.navigationDelegate = self
}
#endif
}
#if DEBUG
extension DevBridgeViewController: WKNavigationDelegate {
func webView(
_ webView: WKWebView,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.performDefaultHandling, nil)
}
}
}
#endif
#if DEBUG 매크로에 주의하세요. 개발 중에만 활성화하기 위한 것이고 프로덕션에서는 최적화로 제거됩니다. 그러지 않으면 프로덕션에서도 모든 인증서를 허용하게 되는데, 당연히 안전하지 않습니다.
FlowChat 의 기억 계층 실험
LemonNeko 가 FlowChat 에서 진행한 기억 계층 실험 결과를 보여 드리겠습니다:
영상에서 저는 LLM 에게 제 이름을 기억하라고 했습니다. 답변을 생성한 뒤 설정 화면에서 기억했다는 것을 확인할 수 있었고, 새 대화를 시작해도 여전히 떠올릴 수 있었습니다.
어떻게 구현했을까요? 현재 구현은 꽤 단순합니다:
- 기억 테이블을 만듭니다.
- LLM 에게 도구 함수를 제공합니다. 기억해야 할 것이 있다고 판단하면 무엇을 기억할지 서술문으로 요약한 뒤 이 도구 함수를 호출합니다.
- 매번 새 답변을 요청할 때 모든 기억을 시스템 프롬프트에 이어 붙입니다.
프롬프트를 어떻게 동적으로 이어 붙일까요? @velin-dev/vue 패키지를 썼습니다. Vue 로 프롬프트를 작성할 수 있게 해 주고, Vue 가 가진 모든 능력을 그대로 쓸 수 있습니다.
prompt.velin.md
<script setup lang="ts">
const props = defineProps<{
memory: string[]
}>()
</script>
<!-- 다른 내용 -->
## Your memories
<ul>
<li v-for="memory in props.memory">{{ memory }}</li>
</ul>
<!-- 다른 내용 -->
위 코드는 markdown 작성도 지원합니다.
혹시 눈치채셨나요? 단계를 소개할 때 "모든 기억을 프롬프트에 이어 붙인다"고 했습니다. 기억이 늘어나면 이 프롬프트는 점점 길어집니다. 어떻게 최적화할까요? 모르겠습니다. 어쩌면 다음 DevLog 의 내용이 될지도요.
맺으며
자, 올해 첫 DevLog 를 제가 대충 썼습니다. 즐겁게 읽으셨기를 바랍니다.
다음 DevLog 에서 만나요.
커버 이미지는 Google Gemini 로 생성했습니다
