AI 물 사용량 떡밥의 문제는, 그 이야기를 하는 사람의 상당수가 AI의 결과물은 모조리 무가치하다는 생각을 하고있다는 것이다. 결과물의 가치가 0이라면, 뭐 물을 500ml만 써도 낭비는 낭비겠지요? AI와 관련해 해결해야할 문제가 한두개가 아닌데, 논의에 질을 높이고 고품격의 토큰이 오갔으면 좋겠다.
bgl gwyng
@bgl@hackers.pub · 99 following · 124 followers
GitHub
- @bglgwyng
(아무도 관심없겠지만) 요거의 예시로는 tree-sitter의 파싱 테이블 + AST node 메타데이터 export가 있다. 지금은 파싱 테이블 대신 parser.c만 뱉고 중요한 메타데이터를 몇개 빼먹는다. 하는 김에 rewrite in haskell도 하면 더 좋고..
NextJS 처음 써보는데, 챗봇 UI처럼 인터액티브한 웹앱을 만들때 도움이 되는 부분이 뭔지를 모르겠다. 처음엔 SPA + API 서버 만드는것과 비교해, 자명한 데이터 바인딩 보일러플레이트를 줄여줄거라 생각했다. 근데, 순수 SSR로 처리할 수 없는, 클라에서 상태를 업데이트하는 약간만 복잡한 플로우에서도 전혀 도움이 안된다.
@bglbgl gwyng 조금 박한 평가일지는 모르겠지만 NextJS는 SEO가 중요한 거 아니면 굳이라는 생각입니다. SSR을 써야 하는 경우가 요즘에는 상당히 제한적이라는 게 제 생각입니다. 굳이 따지자면 프론트엔드랑 백엔드랑 같이 작업할 수 있다 정도인데, 이건 풀스택이 아닌가 싶기도 하고요.
좀 웃긴 생각이 났는데, '해결해주면 나랑 친해질수 잇는 문제들'이란 목록을 만들어서 공유하고 싶다. 네임드가 되면 실제로 잘 동작하려나.
오늘의 권장 행동
랜덤채팅, x톡 이런데에 아래 전화번호 뿌리기
010-3987-0378
010-7955-5680
뿌릴수록 보이스피싱 피해자가 줄어듦
오랜만에 카메라를 들고 산책을 다녀왔습니다. 메리 크리스마스 이브입니다!
https://news.hada.io/topic?id=25285
추상화가 강력한 더 좋은 언어를 쓰는 것 이상의 확실한 방법이 없고, 그 외에는 그냥 임시방편이라고 생각함.
Found this helpful resource by Ben Boyter (@boyter): a collection of sequence diagrams explaining how #ActivityPub/#WebFinger works in practice—covering post creation, follows, boosts, deletions, and user migration.
If you're trying to implement ActivityPub, the spec can be frustratingly vague, and different servers do things differently. This aims to be a “clean room” reference for getting federation right.
@bglbgl gwyng 그러면 계속해서 여러 개의 메세지가 나올 때, 한번 스트리밍이 끝나면 어느 인덱스의 메세지에서 최종 output을 확인할 수 있는지 별로인 것부터가 크다고 생각합니다. 그 뿐만이 아니라 요즘 LLM들은 Text랑 Tool Calling 말고도 multimodal 모델들이 많아서 Image나 File도 첨부할 수 있는데 이런 part들은 어떻게 한 메세지에 확장해서 담을 수 있을지 고민한 결과이기도 하구요.
@nebuletoHaze 저는 Message에 isFinal같은 프로퍼티가 있으면 된다고 생각하고, 말씀하신 이미지나 파일 첨부도 그냥 메시지를 쪼개는게 낫다고 생각합니다. 그냥 챗봇(AI 챗 UI가 아니라 디스코드 등에 붙는)이 가질만한 인터페이스를 그대로 가지는게 낫다고 생각해요.
bgl gwyng replied to the below article:
스마일 PRO 라식 수술 후기
자손킴 @jasonkim@hackers.pub
시력교정술을 받은 주변사람들이 신세계라며 추천을 해도 그동안 관심이 없었다. 어렸을때부터 안경을 쓰고 평생을 살아온지라 안경을 쓰는 것이 불편하다고 느껴 본 적이 없었기 때문이다.
라식을 해볼까? 라고 생각이 든 것은 스쿠버 다이빙을 시작하게 되면서였다. 다이빙에 취미를 붙이고나니 가장 불편한게 눈이었다. 렌즈를 끼고 있는게 불편한건 물론이고 아침 바쁜 와중에 렌즈가 안들어가서 진을 다 빼고 하루를 시작하는것도 문제였다. 게다가 렌즈를 부족하게 들고가거나 숙소에 렌즈를 놓고 오는 등 나의 정신머리 때문에 반쪽짜리 다이빙을 하게 되는 일이 반복되면서 시력교정을 해야겠다고 결심했다.
공장식 병원이야 어차피 거기서 거기라는 생각에 병원비교 사이트를 보고 적당한 곳을 골라 검사 예약을 했다. 원하면 당일 수술도 가능하다고 하던데 나는 검사만 먼저 받고 일주일후에 수술을 하기로 했다.
검사는 일반적인 안과 검사와 크게 다른 것은 없었다. 눈에 바람을 쏘는 것을 시작으로 뭔가를 들여다보고 빛을 비추고 등등 예닐곱가지의 검사가 진행되었다. 이어서 시력 검사를 하고 교정 후 도수를 결정했는데 이것은 안경 맞출때와 동일했다.
검사가 끝나고 의사선생님과 상담하며 눈의 상태에 대해서 설명을 들었다. 다행히 안압이나 눈 모양등도 정상이고 각막 두께도 평균이라 라섹, 라식 모두 원하는대로 진행이 가능하다고 했다. 다만 이제 노안이 오고 있기 때문에 돋보기를 쓰는 시간을 조금이라도 더 늦추려면 시력을 약간 낮추고 양눈 중 주로 가까이 보는 눈은 시력을 조금 더 낮춰 교정하는게 좋겠다는 제안을 받았다.
이어서 코디네이터에게 수술 종류와 방법에 대한 설명을 들었다. 나는 회복이 빠른게 최우선이었기 때문에 스마일PRO를 하기로 결정했다.
수술 당일에도 2~3가지의 검사를 다시하고 최종적으로 교정 시력에 대해서도 다시 한 번 확인을 하고 코디네이터에게 수술 후 주의사항과 안약 투여에 대한 설명을 듣는다.
눈에 물이 닿는 것과 격렬한 운동은 일주일 정도 피해야한다. 2주간은 금주하고 한 달동안 과음도 피해야 한다. 목욕탕, 사우나처럼 뜨거운 증기와 물은 한달간 피한다.
의사선생님을 만나 눈 상태에 대해서 최종점검을 하고 수술 대기실로 이동을 하여 위생모와 가운을 입고 잠시 기다리다 수술실로 들어갔다. 간호사님의 안내에 따라 수술장비에 누우면 눈을 감고 있으라 한 후 세팅을 시작하는데, 이때부터는 절대 고개를 들지말라고 한다.
잠시후 눈을 뜨라고 한 뒤 의사가 눈에 마취안약을 넣어줬다. 집게 같은걸로 눈을 벌리고 "이것만 문제 없으면 다른건 잘 참으실 수 있을거에요"라는 소리와 함께 눈앞에서 뭔가가 왔다갔다 하는데 아마도 마취가 되었는지 안구를 건드려 보는 것 같았다. 눈에 아무런 느낌은 없었다.
수술은 오른쪽 눈부터 진행되었다. 왼쪽눈에는 거즈 같은 것을 덮고 오른쪽 눈으로 앞에 보이는 초록불빛을 바라보라고 한다. 초록불빛을 바라볼때 다른 눈을 감으면 눈이 움직일 수 있으니 양쪽눈을 다 뜨되 수술하는 눈으로만 보라고 한다.
처음에는 크고 흐릿하게 보였던 초록불빛이 점점 작고 선명해 지면서 초록점으로 보인다.초록점을 눈의 중앙에 오도록 보라고 하는데, 이때가 가장 어려웠다. 초록점을 눈의 가운데에 오도록 보고 있는데 의사가 거기가 아니니 제대로 보라고 하는것이다. 몇 번을 다시 시도해도 잘 안됐는지 "환자분이 하는 수술인데 협조가 안되면 어떻게 하냐"며 의사가 약간 역정을 냈다.
초록점을 보는데 실패하면 기계가 눈을 잡아줄 수 없고 의사가 직접 조작하여 수술을 해야 하는데, 그러면 아무래도 기계만큼의 정확도가 나오지는 않는 모양이다.
어떻게하면 방향을 맞출 수 있을지 궁리하다 눈알을 오른쪽에서 왼쪽으로 천천히 굴려보다 의사가 됐다고 하면 멈춰보기로 했다. 눈알을 굴리는데 의사가 거기가 맞다고 하였다. 초록점은 안구의 중앙이 아닌 미간 정도의 위치에 있었다. 잘은 몰라도 사람마다 안구의 각도 같은게 차이가 있나보다.
이제 레이저를 조사하니 가만히 있으라 하고 의사와 간호사가 시간을 세어준다. 초록점을 보고 6~7초 정도 있으면 어느새 점이 사라지고 눈앞이 하얗게 보인다. 2~3초가 더 지나자 끝났다며 잘 참았다고 한다. 레이저가 조사되는 동안은 별다른 느낌은 없었다.
다음으로 왼쪽눈을 진행하는 오른쪽보다 훨씬 수월했다. 왼쪽눈도 우선 처음에는 초록점을 눈의 중앙에 오게 바라봤는데 그게 맞았는지 한 번에 진행되었다. 마찬가지로 약 10초정도가 걸려 레이저 조사가 끝났다.
레이저 조사가 끝나면 무언가로 눈을 후비적거리고 주사를 몇 개 놓고 안약등을 넣고 불빛을 쬐어준다. 이것은 레이저 조사가 마지막으로 끝난 왼쪽을 먼저 하고 오른쪽을 하였다. 아마도 각막 조각을 제거하고 소독등의 처치를 하는 것 같았다. 이 과정은 눈 한쪽당 1~2분 정도 걸렸던 것 같다.
모든 과정이 끝나면 간호사의 안내에 따라 장비에서 일어나 이동을 한다. 궁금한 마음으로 주변을 둘러봤는데 세상이 온통 뿌연게 온통 손자국이 번짐 안경을 쓰고있는 기분이었다. 뿌옇긴해도 이전보다 더 선명해진것은 체감이 됐다.
다시 한 번 의사선생님을 만나 눈에 이상이 없나 검사를 받고 몇가지 주의 사항을 들은 후 퇴원을 한다. 초반에는 빛번짐이 있을 수 있고 시력이 한번에 교정시력 만큼까지 잘보이는 건 아니지만 시간이 지나며 계속 더 잘보일 것 이라고 한다. 그리고 이제 눈이 시리기 시작할건데 1~3시간 정도면 가라앉을 거라는 이야기도 들었다.
퇴원하는 길에 약국에 들려서 안약 2종류와 인공눈물을 받았다. 안약은 일주일간 하루 4번을 투여하고 인공눈물은 수시로 넣으라고 한다. 안약을 여러개 넣을때는 최소한 5분 간격을 두고 넣으라고 하는데 종류가 여러개이다보니 안약 넣다보면 하루가 다 간다.
집에 오니 눈시림이 더 심해지고 눈에 다래끼나 나거나 눈썹이 들어간듯한 이물감이 느껴져서 눈을 뜨고 있을 수가 없었다. 눈을 떠도 온통 뿌옇게 보여서 사물의 형체는 분간이 되지만 글씨 같은건 읽을 수가 없었다. 안약을 넣어야 하는데 주의사항이 적혀있는 종이를 읽을 수 없어서 제미나이 라이브를 켜고서 읽어달라고 했다.
안약을 넣고서 침대에 누워 눈을 감았다. 눈을 감아도 빛이 밝으면 눈이 시려서 빛을 차단하고 누워있다 두어시간 자고 일어났다. 이물감은 여전했지만 눈시림과 빛번짐이 덜해서 눈을 뜨고 무언가를 볼 수는 있었다. 눈을 오래 뜨고 있으면 피로감이 있는건 마찬가지라 저녁 먹고 다시 안약을 넣고 일찍 잠을 청했다. 덕분에 밀린 수면 부채를 많이 갚았다.
눈의 피로감 때문인지 깊게는 못자고 자다 두어번 깼다. 일어나보니 새벽 5시쯤 되었는데 더이상 잠이 안오길래 후기나 써야겠다고 생각했다. 이물감이나 눈시림은 많이 나아졌고 눈도 어제보다는 더 선명하게 보이기 시작했다. 그러나 빛번짐으로 인해 탁하고 뿌옇게 보이는건 여전했다.
모니터나 스마트폰의 화면을 볼 수는 있는데 밝으면 눈이 아프고 집중하면 눈이 시려서 글자 크기를 키우고 화면 밝기는 최대한 낮췄다. 모니터를 오래 보면 눈이 금방 피로해져서 드문드문 후기를 적다가 진료 시간이 다되어 다시 병원을 찾았다.
라식 수술 후에는 1일, 1주일, 1개월, 3개월에 진료를 받는 것을 권장하고 6개월 이후에는 6개월~1년 주기로 한 번씩 검사를 받는 것을 권장한다고 한다.
불편한게 있는지 물어봐서 눈시림과 이물감이 있었지만 지금은 많이 좋아졌다고 답하였다. 먼거리와 가까운 거리의 시력을 다시 한 번 검사하고 의사선생님을 만나 안구 상태에 대해서 진료를 받았다. 어제 수술 중 초록점을 바라보는 문제에 대해 이야기를 나눴다.
내가 걱정했던 부분은 혹여나 잘못된 위치를 바라봐서 각막이 엉뚱하게 절삭된 것은 아닐까 하는 것이었다. 하루가 지나면서 그렇지는 않을 것 같다는 생각을 했으나 혹시 모를 일이라 한 번 더 의사선생님한테 물어봤다.
다행히 어제 수술도 정확히 되었고 오늘 안구 상태도 이상 없으니 걱정 말라는 답을 들었다. 혹여나 초록점을 제대로 못보는 환자가 있다면 나처럼 안구의 중앙이 아닌 다른 곳을 봐야 하는 것일 수 있으니 다른 곳을 보도록 유도하며 안구를 맞추면 좋겠다는 말씀을 드리고 다음주에 뵙자하고 진료를 마무리했다.
시간이 지날수록 시야가 점점 선명해 지는게 체감이 되고 있다. 그러나 뿌옇게 보이는 느낌은 아직 남아 있어서 안경을 닦거나 고쳐써야 할 것 같은데 그럴 안경이 없어서 당혹스러움을 느끼고 있다.
나는 이제 지금까지와는 다른 눈으로 세상을 보게 되었다.
@jasonkim자손킴 저도 10년전쯤에 스마일 라식 했는데 반갑네요ㅎㅎ 주변에서 이야기나오면 꼭 추천합니다.
전 LangChain / LangGraph에 비하면 Vercel AI SDK가 훨씬 sane하다고 생각합니다 ^_ㅜ… 원래 에이전트를 LangChain으로 만들다 이제 Vercel AI SDK에 정착했습니다.
Tool calling은 무엇을 써서 만들어도 안에선 결국 LLM이 호출할 함수와 실행 인자 토큰을 보내면 그걸 provider의 SDK나 그걸 감싼 라이브러리(e.g. LangChain or AI SDK)에서 schema validation을 하고 실패하면 retry를 시키고 하면서 사실상 자기 혼자서 멀티 턴처럼 수행합니다.(그러다가 루프가 나거나 하지 않도록 안에서 상태 머신을 만들고 depth라던가 제한하는거구요.) 그리고 각 파트가 업데이트되고 토큰을 계속 업데이트하다가 동작이 모두 끝나면 그 Part 속 전체 메세지와는 별개의 Message 전체 출력이 나온다는 점에서 이 방식이 맞다고 생각합니다.
저흰 클라이언트와 서버가 굉장히 구분된 편이긴 한데, 서버에서 SSE로 스트림 돌면서 text-delta이면 토큰 이벤트로 추가된 토큰을 내려주고, 스트림이 다 끝나면 전체 텍스트를 가져와서 완료 이벤트로 전체 텍스트를 한번 더 내려주고 대화 제목 업데이트가 필요하면 대화 제목 만들어서 대화 제목 이벤트 보내주고 완료 시점에 데이터베이스에 저장하고 완료 이벤트를 내리고 있습니다. + 클라이언트의 SSE 연결이 끊어져도 해당 작업은 계속 백그라운드에서 돌게만 해두었습니다.
아마 저희가 AI SDK의 프론트엔드 컴포넌트를 쓰지 않고 별도로 자체적으로 이벤트 목록과 스키마를 정해서 그걸로 컴포넌트를 만들어서 크게 스트레스를 못 받는 것일지도 모르겠네요.
@nebuletoHaze 멀티턴을 Message 여러개하는 걸로 할수있지 않나요? 멀티턴을 위해 Part가 필요한지 모르겠습니다.
별개로 말씀하신 구현이 저의 구현이 비슷할거 같습니다. 저도 Vercel AI SDK는 그냥 프로바이더들의 공통 인터페이스로만 쓰고 실제로 delta다루는 로직은 직접 다 하고있습니다.
@bglbgl gwyng 전 그래도 LangChain으로 민드는 것보다 압도적으로 편한 경험이라 생각하고 있는데(Next.js는 아니고 SSE로 내려주는 형태의 백엔드입니다.) 어떤 점이 아쉽거나 힘들다고 느껴지나요??
@nebuletoHaze Vercel AI SDK에 대한 불만은 인용에 써놨구요, 챗봇 UI에서는 서버에서 상태를 스트리밍으로 받와야하는데(저도 SSE 쓰고 있습니다), 이 시나리오에서 NextJS가 사실상 도움이 안됩니다.
NextJS 처음 써보는데, 챗봇 UI처럼 인터액티브한 웹앱을 만들때 도움이 되는 부분이 뭔지를 모르겠다. 처음엔 SPA + API 서버 만드는것과 비교해, 자명한 데이터 바인딩 보일러플레이트를 줄여줄거라 생각했다. 근데, 순수 SSR로 처리할 수 없는, 클라에서 상태를 업데이트하는 약간만 복잡한 플로우에서도 전혀 도움이 안된다.
NextJS + Vercel AI SDK로 챗봇 UI 만들다가 수명이 줄겠네..
@bglbgl gwyng Nix의 대안으로 Guix도 종종 거론되던데, 혹시 살펴보신 적 있으실까요?
@hongminhee洪 民憙 (Hong Minhee) 비슷한 접근이란 것만 알고 있습니다. 남은 문제를 개선하는데 어느쪽이 더 좋은 설계를 갖고있는지는 모르겠네요. 근데 Nix가 일종의 기본 레지스트리에 해당하는 nixpkgs의 규모가 더 클거 같은데, 그러면 당장 쓰기엔 수고가 덜할거 같네요.
그동안 Nix 쓰면서 고통도 많이 받았는데 그래도 주변에 꾸준히 Nix를 권한다. Nix가 '빌드'라는 소프트웨어 개발의 아주 일반적인 문제를 한방에 푸는 방법론이기 때문이다. 물론 아직 몇가지 문제가 좀 있지만(잘 안된다, 불편하다 식의 단순한 문제는 아니다), 나 자신도 그 해결책을 위한 몇가지 아이디어를 가지고 있고, 머지않은 미래에 풀릴거라 생각한다.
UNIX 철학이 작은 기능을 확실하게 수행하는 프로그램들을 만들어 조합하자인데, ls, cat, grep 등이 그 예시다. 내가 볼땐 Nix도 생각도 거기에 해당된다. Nix도 좋은 의미로 의외로 꽤 작다.
@hongminhee洪 民憙 (Hong Minhee) 오... 감사합니다. 한번 살펴보겠습니다. 사실 Vercel AI SDK는 첫삽을 이걸로 떠버려서 어쩔수없이 쓰고있는 상태입니다. 급한 불만 끄고 좀더 나은 라이브러리로 갈아타려고 했어요.
@hongminhee洪 民憙 (Hong Minhee) Vercel AI SDK에서 제가 문제점이라고 느낀 디자인을 그대로 갖고 있네요. 사실 저도 아직 충분히 고민해보진 못했고 반대 의견은 매우 환영입니다.
제가 문제라고 느낀 부분은 Message 타입 밑에 Part가 있는 건데요. 그러니까 LLM의 응답이 플랫하게 Message[]이 아니라 Message[].Part[]가 됩니다. Part는 Plain Text거나 Tool Call일 수 있습니다. 그런데 이게 메시지를 DB에 저장하고 Streaming UI를 만들때 불편합니다. 그냥 Part를 없애고 Message만 있으면 좋겠어요.
처음에 저런식의 디자인을 한 동기를 추측해보자면, Message[]를 User/Assitant/User/Assistant/... 이렇게 번갈아 나타나는 형태를 기대하고, 그걸 만족시키려면 Assistant/Assitant 이렇게 연달아 나타나는걸 피해야하니 Part를 도입한게 아닌가 싶습니다. 근데 실제론 저 번갈아 나타나야한다는 조건이 타입으로 강제도 안되고(이건 어려우니 OK) 런타임에서 뭐라고 하지도 않아요. 그리고 실제 사용에서 연달아 나타나는걸 허용하는게 오히려 자연스럽습니다.
그래서 처음에 잠깐 잘못 생각해서 나온 디자인이, 실제론 의도한 제약을 주고있지도 못하고 그냥 쓰임만 불편하게 만들고 있는거 같습니다.
@hongminhee洪 民憙 (Hong Minhee) 오... 감사합니다. 한번 살펴보겠습니다. 사실 Vercel AI SDK는 첫삽을 이걸로 떠버려서 어쩔수없이 쓰고있는 상태입니다. 급한 불만 끄고 좀더 나은 라이브러리로 갈아타려고 했어요.
Vercel AI SDK는 LLM이라는 훌륭한 기술을 주옥같은 인터페이스로 감싸놓았다. 정말 이해가 안가는 추상화 투성이다.
@hongminhee洪 民憙 (Hong Minhee) 안타깝게도 내일은 참석을 못합니다ㅠㅠ 즐거운 시간 보내십셔..
으악 송년회 오늘인줄알고 헛걸음했네
요즘 개발자들 만나면 죄다 AI 얘기밖에 안하는데, 대부분 ‘동물원 가서 코끼리 봤어’ 수준의 이야기라 다 들어주기가 피곤하다. 여기서 들은 얘기 저기서 또 들어야하고.
bgl gwyng shared the below article:
Claude API의 Request Body 분석
자손킴 @jasonkim@hackers.pub
Claude API의 Request는 크게 4가지 분류를 가지고 있다.
- System Messages
- Messages
- Tools
- Model & Config
각각은 다음과 같은 역할을 한다.
System Messages
System Messages는 Claude에게 역할, 성격, 제약사항 등을 지시하는 최상위 설정이다. 배열 형태로 여러 개의 시스템 메시지를 전달할 수 있다.
"system": [
{
"type": "text",
"text": "You are Claude Code, Anthropic's official CLI for Claude.",
"cache_control": {
"type": "ephemeral"
}
},
{
"type": "text",
"text": "You are an interactive CLI tool that helps users with software engineering tasks...",
"cache_control": {
"type": "ephemeral"
}
}
]
System Messages에는 다음과 같은 내용이 포함된다:
- Claude의 페르소나 및 역할 정의
- 보안 및 윤리 가이드라인
- 응답 형식 및 톤 설정
- 프로젝트 정보 등 컨텍스트
cache_control을 통한 캐싱 설정
Messages
Messages는 user와 assistant 역할이 번갈아가며 주고받은 대화를 누적하는 배열이다. assistant 메시지는 반드시 모델의 실제 응답일 필요가 없다. 이를 활요해 API 호출 시 assistant 메시지를 미리 작성해서 전달하면, Claude는 그 내용 이후부터 이어서 응답한다. 이를 Prefill 기법이라 한다.
이 대화 기록을 통해 Claude는 맥락을 유지하며 응답한다.
"messages": [
{
"role": "user",
"content": [...]
},
{
"role": "assistant",
"content": [...]
},
{
"role": "user",
"content": [...]
}
]
User Message
User의 content는 주로 두 가지 type으로 구성된다:
1. text - 사용자의 일반 메시지나 시스템 리마인더
{
"role": "user",
"content": [
{
"type": "text",
"text": "선물을 주고받는 기능을 위한 entity를 설계하라."
}
]
}
2. tool_result - Tool 실행 결과 반환
{
"role": "user",
"content": [
{
"tool_use_id": "toolu_01Qj7gnFLKWBNjg",
"type": "tool_result",
"content": [
{
"type": "text",
"text": "## Entity 구조 탐색 보고서\n\n철저한 탐색을 통해..."
}
]
}
]
}
Assistant Message
Assistant의 content는 주로 세 가지 type으로 구성된다:
1. text - Claude의 응답 메시지
{
"type": "text",
"text": "선물 주고받기 기능을 위한 entity 설계를 시작하겠습니다."
}
2. thinking - Extended Thinking 기능 활성화 시 사고 과정 (signature로 검증)
{
"type": "thinking",
"thinking": "사용자가 선물을 주고받는 기능을 위한 entity 설계를 요청했습니다...",
"signature": "EqskYIChgCKknyFYp5cu1zhVOp7kFTJb..."
}
3. tool_use - Tool 호출 요청
{
"type": "tool_use",
"id": "toolu_01Qj7gn6vLKCNjg",
"name": "Task",
"input": {
"subagent_type": "Explore",
"prompt": "이 NestJS TypeScript 프로젝트에서 entity 구조를 탐색해주세요...",
"description": "Entity 구조 탐색"
}
}
User와 Assistant의 협력
Tool 사용 흐름은 다음과 같이 진행된다:
- Assistant:
tool_use로 Tool 호출 요청 - User:
tool_result로 실행 결과 반환 - Assistant: 결과를 바탕으로
text응답 또는 추가tool_use
이 과정에서 어떤 Tool을 사용할 수 있는지는 tools 배열이 정의한다.
Tools
Tools는 Claude가 사용할 수 있는 도구들을 정의하는 배열이다. 각 Tool은 name, description, input_schema 세 가지 필드로 구성된다.
Tool의 기본 구조
"tools": [
{
"name": "ToolName",
"description": "Tool에 대한 설명...",
"input_schema": {
"type": "object",
"properties": {...},
"required": [...],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
]
| 필드 | 설명 |
|---|---|
name |
Tool의 고유 식별자. Claude가 tool_use에서 이 이름으로 호출 |
description |
Tool의 용도, 사용법, 주의사항 등을 상세히 기술. Claude가 어떤 Tool을 선택할지 판단하는 근거 |
input_schema |
JSON Schema 형식으로 입력 파라미터 정의 |
input_schema 구조
input_schema는 JSON Schema draft-07 스펙을 따르며, Tool 호출 시 필요한 파라미터를 정의한다.
"input_schema": {
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "The regular expression pattern to search for"
},
"path": {
"type": "string",
"description": "File or directory to search in. Defaults to current working directory."
},
"output_mode": {
"type": "string",
"enum": ["content", "files_with_matches", "count"],
"description": "Output mode: 'content' shows matching lines, 'files_with_matches' shows file paths..."
},
"-i": {
"type": "boolean",
"description": "Case insensitive search"
},
"head_limit": {
"type": "number",
"description": "Limit output to first N lines/entries"
}
},
"required": ["pattern"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
properties 내 각 파라미터 정의
각 파라미터는 다음 필드들로 정의된다:
| 필드 | 설명 |
|---|---|
type |
데이터 타입 (string, number, boolean, array, object 등) |
description |
파라미터의 용도와 사용법 설명 |
enum |
(선택) 허용되는 값의 목록. 이 중 하나만 선택 가능 |
default |
(선택) 기본값 |
input_schema의 메타 필드
| 필드 | 설명 |
|---|---|
type |
항상 "object" |
properties |
파라미터 정의 객체 |
required |
필수 파라미터 이름 배열. 여기 포함되지 않은 파라미터는 선택적 |
additionalProperties |
false면 정의되지 않은 파라미터 전달 불가 |
$schema |
JSON Schema 버전 명시 |
실제 예시: Grep Tool
{
"name": "Grep",
"description": "A powerful search tool built on ripgrep\n\n Usage:\n - ALWAYS use Grep for search tasks...",
"input_schema": {
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "The regular expression pattern to search for in file contents"
},
"path": {
"type": "string",
"description": "File or directory to search in (rg PATH). Defaults to current working directory."
},
"glob": {
"type": "string",
"description": "Glob pattern to filter files (e.g. \"*.js\", \"*.{ts,tsx}\")"
},
"output_mode": {
"type": "string",
"enum": ["content", "files_with_matches", "count"],
"description": "Output mode. Defaults to 'files_with_matches'."
},
"-A": {
"type": "number",
"description": "Number of lines to show after each match"
},
"-B": {
"type": "number",
"description": "Number of lines to show before each match"
},
"-i": {
"type": "boolean",
"description": "Case insensitive search"
},
"multiline": {
"type": "boolean",
"description": "Enable multiline mode. Default: false."
}
},
"required": ["pattern"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
이 Tool을 Claude가 호출할 때의 tool_use:
{
"type": "tool_use",
"id": "toolu_01ABC123",
"name": "Grep",
"input": {
"pattern": "class.*Entity",
"path": "src/modules",
"glob": "*.ts",
"output_mode": "content",
"-i": true
}
}
required에 pattern만 있으므로 나머지는 선택적이다. Claude는 input_schema의 description을 참고하여 적절한 파라미터를 선택한다.
Model & Config
마지막으로 모델 선택과 각종 설정 옵션들이다:
{
"model": "claude-opus-4-5-20251101",
"max_tokens": 32000,
"thinking": {
"budget_tokens": 31999,
"type": "enabled"
},
"stream": true,
"metadata": {
"user_id": "user_2f2ce5dbb94ac27c8da0d0b28dddf815fc82be54e0..."
}
}
| 옵션 | 설명 |
|---|---|
model |
사용할 Claude 모델 (claude-opus-4-5, claude-sonnet-4-5 등) |
max_tokens |
최대 출력 토큰 수 |
thinking |
Extended Thinking 설정 (budget_tokens로 사고 토큰 예산 설정) |
stream |
스트리밍 응답 여부 |
metadata |
사용자 ID 등 메타데이터 |
마치며
지금까지 Claude API Request Body의 4가지 핵심 구성 요소를 살펴보았다:
- System Messages: Claude의 역할과 행동 방식을 정의
- Messages: user-assistant 간 대화 기록을 누적하며, tool_use/tool_result를 통해 Tool과 상호작용
- Tools: JSON Schema 기반으로 사용 가능한 도구의 이름, 설명, 입력 파라미터를 정의
- Model & Config: 모델 선택, 토큰 제한, 스트리밍 등 설정
이 구조를 알면 Claude가 주고받은 메시지를 어떻게 관리하는지, 도구를 어떻게 사용하는지 이해하고 API를 더 효과적으로 활용할 수 있다.
"OOO 주식회사 OOO 대표님 맞으시죠? 산업안전보건교육 받아야 하는 업체에 해당합니다. 직원이나 프리랜서 고용 있으시죠? 어쩌고~" 일단 쎄한 느낌을 받았습니다. 다다다 쏴붙이면서 중간에 질문할 틈을 최대한 막으면서 가는 꼬락서니가 관공서는 아니구나 싶었습니다. 고용이나 프리랜서 없다고 하니, 뚝 끊어 버립니다. 검색해보니, 이런식의 반쯤 사기같은 행태가 존재한다고 하네요. 소규모 법인 대표님들 조심하세요~
@bglbgl gwyng 그래서 하스켈을 쓰고 싶은데... 하스켈 붐은 도대체 언제 올까요ㅠㅠ
@2chanhaeng초무 더이상 앉아서 붐을 기다릴수 없습니다
Zed 에디터 써보는 중인데 Git 연동이 좀 부실하다. 보통은 GitKraken이나 lazygit 등의 도구를 병행해서 쓰니까, 꼭 에디터에서 Git을 빵빵하게 지원해야하냐는 의문은 든다. 다만 Git의 특정 기능들은 에디터 연동이 불가피한데(diff 보기, conflict 해결하기 등), 이런거보면 에디터가 Git의 인터페이스가 되어야하는거 같기도하고 그렇다.
Nushell 반나절동안 썼는데, 지금까지 문제는... 그 리치한 기능을 전혀 안 썼다는것이다. 그래서 단지 zsh와 비교해 익숙하지않음+플러그인부족에서 기인한 불편함만 체험하고 말았다.
언제까지 (a:number, b:number) => a + b, (a:string, b:string) => a + b, <T>(a: T, b: T) => a + b 를 해줘야 하나고
그냥 대충 눈치껏 (a, b) => a + b 하면 'b 는 a 와 더할 수 있어야 하는 타입이고 a 는 무언가와 더할 수 있는 타입이구나' 하고 추론할 수 있는 분석기가 달린 언어가 필요함
@2chanhaeng초무 하스켈 있음다..
사상 최고의 크리스마스 영화: 다이하드 2.
@meWoojin Kim 1이 아니라요?
#연합당근
하드디스크 처분합니다.
모든 제품 베드섹터 없으며 정상동작 확인 했습니다.
전문 업체가 아니다 보니 자체 A/S는 불가 하며 중고입니다.
각 디스크 별 상태 정보도 같이 첨부 드립니다.
택배비 미 포함 가격이며 택배비는 상황에 따라 일정하지 않아서 제가 따로 알려드리겠습니다.
1. 500GB 16MB 7200RPM 삼성 2만원(학생 할인가 1만원)
2. 250GB 8MB 7200RPM WD 1만 5천원(학생 할인가 5천원)
3. 160GB 8MB 7200RPM 씨게이트 무료
씨게이트는 멀쩡한걸 확인했지만 아무래도 씨게이트인지라 무료 입니다.
글풍선에 작성자 프로필 이미지를 이제야 붙였습니다. 붙이고 나니, 필수 요소겠구나 하는 생각이 듭니다. 작성한 글은 자신을 표현하는 일부인데, 내 글인지 표시가 없으면, 내가 누군가에게 표현했다는 느낌이 반감되는 것 같습니다.
yearit.com
primes :: (Integral a) => [a]
primes = 2 : ([3, 5 ..] & filter (not . has_divisor))
where
has_divisor n =
any ((0 ==) . (n `mod`) . fst) $ takeWhile ((n >=) . snd) primes_with_square
primes_with_square :: (Integral a) => [(a, a)]
primes_with_square = [(p, p * p) | p <- primes]
euler project 문제 풀다가..
리액트의 dumb component는 이름과달리 약간은 더 똑똑할 필요가 있는데. dumb component는 업데이트를 반드시 부모를 통해서만 해야한다. 이때 fine-grained reactivity로 성능을 높이려면 (딱히 별 하는 일도 없는) wrapper가 필요하다. 그리고 데이터 페칭과 관련될 경우 또 wrapper를 반드시 만들어 줘야한다.
이걸 어떻게 해결할수 있나? dumb component가 Props로 raw value가 아닌 signal을 받게하는 것이다. 아쉽게도 현재 JS에 표준 Signal 인터페이스가 없기에 jotai atom 등을 써야하는데, 그러면 컴포넌트가 프레임워크에 의존하게 되어 덜 dumb해지는 문제가 있다.
어쩌다 커서, 윈드서프 대신에 오리지널 VS Code + 코파일럿을 쓰고 있는데, AI의 UX 통합이랑 측면에선 이쪽이 더 낫네? 윈드서프는 채팅 UI 벗어나면 잡버그때문에 못쓸 수준이다.
https://github.com/yamadashy/repomix/releases/tag/v1.10.0
Repomix가 소스코드를 압축해서 LLM 친화적인 텍스트를 뽑아주는 CLI도구인데, 이제 그걸 넘어서 Claude Code Skill도 뽑아주는 기능이 추가되었다 .....
유튜브 링크를 넣을 수 있게 했더니, 아래처럼 쓰기도 합니다.
추천 영상, 댓글, 다음 영상 같은 게 안보여서, 조용한 카페같다네요. 제가 의도했던 건 아닌데, 저도 써보니 그런 것 같기도 합니다. 글들에 있는 링크 연속 재생 방법을 찾는 숙제가 생겼습니다. http://yearit.com/note/myplaylist
@bglbgl gwyng 저는 제가 만든 프로젝트들의 체인지로그를 추천합니다… ㅋㅋㅋㅋ
@hongminhee洪 民憙 (Hong Minhee) 다르긴 다르네요ㅋㅋ
사실 이래서 나는 커밋 메시지로부터 어떻게든 체인지로그를 자동으로 생성해 내려는 일체의 시도에 회의적임…
@hongminhee洪 民憙 (Hong Minhee) 반대로 좋은 예시는 어떤게 있을까요? 사실 제가 관심있게 지켜보던 레포는 체인지로그 저런식이어도 대충 알아듣겠더라고요.
bgl gwyng shared the below article:
Stop writing if statements for your CLI flags
洪 民憙 (Hong Minhee) @hongminhee@hackers.pub
If you've built CLI tools, you've written code like this:
if (opts.reporter === "junit" && !opts.outputFile) {
throw new Error("--output-file is required for junit reporter");
}
if (opts.reporter === "html" && !opts.outputFile) {
throw new Error("--output-file is required for html reporter");
}
if (opts.reporter === "console" && opts.outputFile) {
console.warn("--output-file is ignored for console reporter");
}
A few months ago, I wrote Stop writing CLI validation. Parse it right the first time. about parsing individual option values correctly. But it didn't cover the relationships between options.
In the code above, --output-file only makes sense when --reporter is junit or html. When it's console, the option shouldn't exist at all.
We're using TypeScript. We have a powerful type system. And yet, here we are, writing runtime checks that the compiler can't help with. Every time we add a new reporter type, we need to remember to update these checks. Every time we refactor, we hope we didn't miss one.
The state of TypeScript CLI parsers
The old guard—Commander, yargs, minimist—were built before TypeScript became mainstream. They give you bags of strings and leave type safety as an exercise for the reader.
But we've made progress. Modern TypeScript-first libraries like cmd-ts and Clipanion (the library powering Yarn Berry) take types seriously:
// cmd-ts
const app = command({
args: {
reporter: option({ type: string, long: 'reporter' }),
outputFile: option({ type: string, long: 'output-file' }),
},
handler: (args) => {
// args.reporter: string
// args.outputFile: string
},
});
// Clipanion
class TestCommand extends Command {
reporter = Option.String('--reporter');
outputFile = Option.String('--output-file');
}
These libraries infer types for individual options. --port is a number. --verbose is a boolean. That's real progress.
But here's what they can't do: express that --output-file is required when --reporter is junit, and forbidden when --reporter is console. The relationship between options isn't captured in the type system.
So you end up writing validation code anyway:
handler: (args) => {
// Both cmd-ts and Clipanion need this
if (args.reporter === "junit" && !args.outputFile) {
throw new Error("--output-file required for junit");
}
// args.outputFile is still string | undefined
// TypeScript doesn't know it's definitely string when reporter is "junit"
}
Rust's clap and Python's Click have requires and conflicts_with attributes, but those are runtime checks too. They don't change the result type.
If the parser configuration knows about option relationships, why doesn't that knowledge show up in the result type?
Modeling relationships with conditional()
Optique treats option relationships as a first-class concept. Here's the test reporter scenario:
import { conditional, object } from "@optique/core/constructs";
import { option } from "@optique/core/primitives";
import { choice, string } from "@optique/core/valueparser";
import { run } from "@optique/run";
const parser = conditional(
option("--reporter", choice(["console", "junit", "html"])),
{
console: object({}),
junit: object({
outputFile: option("--output-file", string()),
}),
html: object({
outputFile: option("--output-file", string()),
openBrowser: option("--open-browser"),
}),
}
);
const [reporter, config] = run(parser);
The conditional() combinator takes a discriminator option (--reporter) and a map of branches. Each branch defines what other options are valid for that discriminator value.
TypeScript infers the result type automatically:
type Result =
| ["console", {}]
| ["junit", { outputFile: string }]
| ["html", { outputFile: string; openBrowser: boolean }];
When reporter is "junit", outputFile is string—not string | undefined. The relationship is encoded in the type.
Now your business logic gets real type safety:
const [reporter, config] = run(parser);
switch (reporter) {
case "console":
runWithConsoleOutput();
break;
case "junit":
// TypeScript knows config.outputFile is string
writeJUnitReport(config.outputFile);
break;
case "html":
// TypeScript knows config.outputFile and config.openBrowser exist
writeHtmlReport(config.outputFile);
if (config.openBrowser) openInBrowser(config.outputFile);
break;
}
No validation code. No runtime checks. If you add a new reporter type and forget to handle it in the switch, the compiler tells you.
A more complex example: database connections
Test reporters are a nice example, but let's try something with more variation. Database connection strings:
myapp --db=sqlite --file=./data.db
myapp --db=postgres --host=localhost --port=5432 --user=admin
myapp --db=mysql --host=localhost --port=3306 --user=root --ssl
Each database type needs completely different options:
- SQLite just needs a file path
- PostgreSQL needs host, port, user, and optionally password
- MySQL needs host, port, user, and has an SSL flag
Here's how you model this:
import { conditional, object } from "@optique/core/constructs";
import { withDefault, optional } from "@optique/core/modifiers";
import { option } from "@optique/core/primitives";
import { choice, string, integer } from "@optique/core/valueparser";
const dbParser = conditional(
option("--db", choice(["sqlite", "postgres", "mysql"])),
{
sqlite: object({
file: option("--file", string()),
}),
postgres: object({
host: option("--host", string()),
port: withDefault(option("--port", integer()), 5432),
user: option("--user", string()),
password: optional(option("--password", string())),
}),
mysql: object({
host: option("--host", string()),
port: withDefault(option("--port", integer()), 3306),
user: option("--user", string()),
ssl: option("--ssl"),
}),
}
);
The inferred type:
type DbConfig =
| ["sqlite", { file: string }]
| ["postgres", { host: string; port: number; user: string; password?: string }]
| ["mysql", { host: string; port: number; user: string; ssl: boolean }];
Notice the details: PostgreSQL defaults to port 5432, MySQL to 3306. PostgreSQL has an optional password, MySQL has an SSL flag. Each database type has exactly the options it needs—no more, no less.
With this structure, writing dbConfig.ssl when the mode is sqlite isn't a runtime error—it's a compile-time impossibility.
Try expressing this with requires_if attributes. You can't. The relationships are too rich.
The pattern is everywhere
Once you see it, you find this pattern in many CLI tools:
Authentication modes:
const authParser = conditional(
option("--auth", choice(["none", "basic", "token", "oauth"])),
{
none: object({}),
basic: object({
username: option("--username", string()),
password: option("--password", string()),
}),
token: object({
token: option("--token", string()),
}),
oauth: object({
clientId: option("--client-id", string()),
clientSecret: option("--client-secret", string()),
tokenUrl: option("--token-url", url()),
}),
}
);
Deployment targets, output formats, connection protocols—anywhere you have a mode selector that determines what other options are valid.
Why conditional() exists
Optique already has an or() combinator for mutually exclusive alternatives. Why do we need conditional()?
The or() combinator distinguishes branches based on structure—which options are present. It works well for subcommands like git commit vs git push, where the arguments differ completely.
But in the reporter example, the structure is identical: every branch has a --reporter flag. The difference lies in the flag's value, not its presence.
// This won't work as intended
const parser = or(
object({ reporter: option("--reporter", choice(["console"])) }),
object({
reporter: option("--reporter", choice(["junit", "html"])),
outputFile: option("--output-file", string())
}),
);
When you pass --reporter junit, or() tries to pick a branch based on what options are present. Both branches have --reporter, so it can't distinguish them structurally.
conditional() solves this by reading the discriminator's value first, then selecting the appropriate branch. It bridges the gap between structural parsing and value-based decisions.
The structure is the constraint
Instead of parsing options into a loose type and then validating relationships, define a parser whose structure is the constraint.
| Traditional approach | Optique approach |
|---|---|
| Parse → Validate → Use | Parse (with constraints) → Use |
| Types and validation logic maintained separately | Types reflect the constraints |
| Mismatches found at runtime | Mismatches found at compile time |
The parser definition becomes the single source of truth. Add a new reporter type? The parser definition changes, the inferred type changes, and the compiler shows you everywhere that needs updating.
Try it
If this resonates with a CLI you're building:
- Documentation
- Tutorial
conditional()reference- GitHub
Next time you're about to write an if statement checking option relationships, ask: could the parser express this constraint instead?
The structure of your parser is the constraint. You might not need that validation code at all.
한 해를 마무리하는 글을 블로그에 썼습니다: 〈聯合宇宙와 함께 한 2025年〉(한글 專用文은 이쪽). 題目 그대로 聯合宇宙와 함께 했던 저의 한 해를 되돌아 보는 글입니다. 聯合宇宙 德分에 많은 因緣과 이어지게 되어서 感謝하게 생각합니다.
Fediverse Advent Calendar 2025の10日目に参加する記事をブログに投稿しました:「フェディバースと過ごした2025年」。タイトルの通り、フェディバースと共に過ごした私の一年を振り返る内容です。フェディバースのおかげで多くのご縁に恵まれ、感謝しています。これからもよろしくお願いします。
Fediverse Advent Calendar 2025の10日目に参加する記事をブログに投稿しました:「フェディバースと過ごした2025年」。タイトルの通り、フェディバースと共に過ごした私の一年を振り返る内容です。フェディバースのおかげで多くのご縁に恵まれ、感謝しています。これからもよろしくお願いします。
I posted a blog entry to wrap up the year: My 2025 with the fediverse. I'm grateful that the fediverse has allowed me to connect with so many people. I look forward to our continued connection.
Vercel 제품들은 딱 봐도 설계가 너무 구리게 느껴지는데, 그렇다고 깊게 생각해보진 않고 또 쨋든 사람들이 젤 많이 쓰긴하니까, 내 느낌이 잘못된건지 헷갈린다.
Released Optique 0.8.0, a type-safe CLI parser for TypeScript.
This version adds conditional() for branching based on a discriminator option, passThrough() for forwarding unknown options to underlying tools, and a new @optique/logtape package for configuring LogTape via CLI.
https://djot.net/ djot이라고 Markdown 개발자가 Markdown의 문제점을 고쳐서 내놓은 마크업 언어이다. 이러고보니 Deno같군.
@bglbgl gwyng 제가 알기로는 Markdown 만든 사람은 아니고 Pandoc 제작자이자 CommonMark 스펙 공동 저자이신 분이예요!
https://djot.net/ djot이라고 Markdown 개발자가 Markdown의 문제점을 고쳐서 내놓은 마크업 언어이다. 이러고보니 Deno같군.
SolidJS로 앱 만들다가 아이콘셋이 필요해져서 패키지를 뒤져보는데, 마이너 생태계답게 마지막 업데이트가 삼사년 전인 패키지들만 나온다. 아이콘셋에 업데이트가 필요 없긴 하지. 그래도 최근에 업데이트 된 패키지가 걸리적거리는게 없을 것 같달까. 그러다 활발히 업데이트 중인 unplugin-icons를 찾았다. 이것은 SolidJS용 패키지가 아니었다. 아이콘셋도 아니었다. 거의 모든 아이콘셋을 거의 모든 프레임워크에서 사용할 수 있게 해주는 도구다. 이런 문물이 있었다니. 누가 만들었나 함 보자. 제작자는 Anthony Fu... 아아 또 그인가. 오늘도 비 React 웹 생태계엔 Anthony Fu의 은혜가 넘친다.
@hongminhee洪 民憙 (Hong Minhee) 기대하겠습니다!













