schemalism
PART 03·of 04

AI 초안과 사람이 실제 전달한 한마디 — 피드백을 두 컬럼으로 쪼갠 이유

ai_feedback_content 와 real_feedback_content 를 굳이 분리한 세 가지 이유. UNSAFE 분류를 AI worker 가 자동 보강하되 사람이 채운 칸은 안 건드리는 규칙, 그리고 코칭의 '발행'과 '전달'을 read_date 한 칸으로 구분한 디테일.

2026년 8월 16일 · 정윤환 · 7

BBS 가 정착된 회사와 정착 안 된 회사의 결정적 차이는 관찰 이 아니라 피드백 에서 갈립니다.

  • 안 정착된 회사 — 관찰자가 카드를 채우고 제출하면 끝. 관찰 대상자는 자기가 관찰됐다는 사실조차 모를 때가 많습니다.
  • 정착된 회사 — 관찰이 끝나면 관찰자가 대상자에게 그 자리에서 “방금 안전모 끈이 풀려있던데, 사다리에서 머리 부딪치면 진짜 크게 다칩니다” 같은 한마디를 건넵니다. 그 한마디가 BBS 의 본체입니다.

문제는 그 한마디를 어떻게 적게 만드느냐 입니다. 관찰자도 사람이라 12개 행동을 평가하고 나면 피곤합니다. 피드백 칸을 비우고 제출 버튼을 누릅니다. 그래서 우리는 AI 가 초안을 만들어 주기로 했어요.

01 · Two columns

두 컬럼을 굳이 분리한 이유

ai_feedback vs real_feedback
자동화의 영역과 사람의 자율 영역
python
class BbsObsFeedback(Base):
    """관찰 1건에 대한 전체 종합 피드백"""
    ...
    ai_feedback_content   = Column(Text, nullable=True,
        comment='AI 가 생성한 초안 피드백')
    real_feedback_content = Column(Text, nullable=True,
        comment='관찰자가 실제로 전달한 피드백')

한 컬럼에 “AI 가 만든 걸 사람이 다듬어서 저장” 하면 더 간단합니다. 그런데 두 컬럼으로 분리하지 않으면 사라지는 정보가 있어요.

  • AI 가 무엇을 제안했는가 — AI 의 제안 품질을 추적하지 못합니다. 다음 모델 버전이 더 나은지 비교할 수 없어요.
  • 사람이 무엇을 바꿨는가 “AI 가 너무 부드럽게 썼는데 관찰자가 더 단호하게 바꿨다” 같은 사용자 행동 패턴이 사라집니다. 이 패턴은 다음 프롬프트 튜닝의 핵심 단서입니다.
  • AI 를 안 쓰고 직접 적었는가ai_feedback_content = NULL 이지만 real_feedback_content 가 채워져 있다면 AI 없이 직접 쓴 관찰자 입니다. 그 사람은 BBS 가 손에 익은 베테랑입니다. 이 분포가 사업장 BBS 성숙도의 신호입니다.

세 정보 모두 데이터 모델에 두 컬럼으로 분리해놓아야 살아남는 정보 입니다. 분석을 위해서가 아니라, 분리해두지 않으면 나중에 분석하고 싶어졌을 때 이미 늦었기 때문 입니다.

02 · Two layers of feedback

행동 1개에 피드백, 관찰 1건에 종합 피드백 — 두 층

피드백은 두 층에 붙습니다.

rel
BbsObs (1) ──→ (N) BbsObsBehavior (1) ──→ (N) BbsObsBehaviorFeedback
   │                                            (행동 1건에 대한 피드백)
   │
   └──→ (N) BbsObsFeedback
              (관찰 전체에 대한 종합 피드백)

행동마다 피드백을 따로 두는 이유는 단순합니다. “안전모 끈은 잘 매고 있었습니다. 그런데 사다리 3단째에 한 손으로 뭘 잡으면서 올라간 건 위험합니다.” 같은 한 관찰 안의 칭찬과 지적이 한 컬럼에 섞이면, 그 행동만 단독으로 빼서 보여줄 때 깨집니다.

종합 피드백은 행동들을 모두 본 다음의 한 문단 입니다. 끝맺음 문장이라 별도로 둡니다. AI 가 모든 행동을 종합해 자연어 한 문단을 만들고, 관찰자가 그 자리에서 다듬어 대상자에게 전달합니다.

03 · Worker fills blanks · never overwrites

Worker 가 사람의 빈칸을 메운다 — 단, 사람이 채운 칸은 안 건드린다

피드백뿐만 아니라 위험 분류 도 사람의 부담입니다. 관찰자가 UNSAFE 만 누르고 위험 이벤트·요소·통제조치 칸을 비워두는 일이 흔합니다. 그래서 우리는 worker 를 띄웠습니다.

python
class BbsObsBehavior(Base):
    risk_analysis_status = Column(
        Enum(BbsObsBehaviorRiskAnalysisStatus),  # NULL/PROCESSING/FAILED/COMPLETED
        nullable=True,
        comment='위험분석 자동 생성 상태. NULL=아직 worker 손길 없음',
    )
    risk_analysis_attempt_count = Column(
        Integer, nullable=False, default=0,
        comment='worker 분석 시도 횟수. 일정 횟수 이상 실패 시 더 이상 재시도하지 않음',
    )
    risk_analysis_last_attempted_at = Column(DateTime, nullable=True,
        comment='worker 가 마지막으로 처리 시도한 시각 (UTC)')
    risk_analysis_failure_reason   = Column(Text, nullable=True,
        comment='최종 실패 사유 (status=FAILED 일 때)')
    is_risk_analysis_ai_generated  = Column(Boolean, nullable=False, default=False,
        comment='위험분석 필드를 AI 가 자동 생성했는지 여부')

worker 의 규칙은 두 줄입니다.

  1. behavior_result=UNSAFE 인데 분류 컬럼이 비어 있으면 큐에 넣는다.
  2. 사용자가 직접 채운 row 는 is_risk_analysis_ai_generated=False 인 채 그대로 두고, 절대 건드리지 않는다.

auto domain

  • UNSAFE 이지만 분류 비어있음
  • AI 가 채움 → is_ai_generated = True
  • 사람이 덮어쓰면 False 로 굳음

human domain

  • 사용자가 직접 채운 row
  • is_ai_generated = False
  • worker 가 절대 손대지 않음

특히 두 번째 규칙이 핵심입니다. AI 자동화의 가장 흔한 사고는 “사용자가 의도적으로 비워둔 칸” 을 AI 가 채우거나 “사용자가 의도적으로 채운 칸” 을 AI 가 덮어쓰는 것입니다. 그래서 AI 가 생성한 row사람이 만진 row 를 boolean 한 칸으로 갈라놓았어요.

04 · Worker must know how to stop

Worker 가 멈출 줄도 알아야 한다

자동화 worker 의 두 번째 흔한 사고는 무한 재시도 입니다. 외부 API 가 죽었을 때, AI 모델이 잘못된 입력으로 영구 실패할 때, worker 가 같은 row 를 5분마다 시도하면 토큰 비용이 무한히 빠집니다.

그래서 risk_analysis_attempt_count risk_analysis_failure_reason 을 같이 박았습니다. worker 는 시도 횟수가 임계값을 넘으면 그 row 를 영구 실패 로 두고 큐에서 뺍니다. risk_analysis_status=FAILED, risk_analysis_failure_reason 에 마지막 사유. 어드민 화면에서 “AI 가 못 메운 행동” 으로 따로 보이고, 관리자가 손으로 채우거나 다시 큐에 넣을 수 있습니다.

“AI 가 알아서 한다”“무한히 시도한다” 사이의 간격을 데이터 모델로 메운 거예요.

05 · Coaching the observer

Coach — BBS 관찰자를 또 누가 코칭한다

BBS 의 마지막 층은 코칭 입니다. 관찰자가 동료를 잘 본 다음, 그 관찰자를 우리(플랫폼) 어드민이 직접 코칭합니다. “이 카드에 SAFE 가 너무 많아요. 보통 일주일에 12건 보면 UNSAFE 가 2~3건은 나오는데, 너무 후한 평가 같습니다.” 같은 한마디를 어드민이 관찰자에게 던집니다.

python
class BbsCoachCard(Base):
    monitoring_admin_user_id = Column(String(36), ...)  # 코칭 수행 어드민
    target_org_member_id     = Column(String(36), ...)  # 코칭 대상
    obs_id                   = Column(String(36), ...)  # 데이터 주체인 관찰 건
    monitoring_order         = Column(Integer, ...)     # 같은 멤버 코칭 회차

    strength_memo       = Column(Text, ...)  # 강점
    improvement_memo    = Column(Text, ...)  # 개선
    conclusion_memo     = Column(Text, ...)  # 결론
    next_practice_memo  = Column(Text, ...)  # 다음 실습 계획

    read_date = Column(DateTime, nullable=True,
        comment='코칭 대상자가 상세 화면에 진입한 시각')

monitoring_order 가 회차를 추적합니다. 같은 멤버에게 1차 코칭에서 “안전모 결속을 더 꼼꼼히 봅시다” 라고 했으면, 3차 코칭 때 그게 정착됐는지 보면 됩니다.

read_date 한 줄이 인상적입니다. 어드민이 코칭 카드를 발행해도, 대상자가 상세 화면에 진입한 시각 이 없으면 이 코칭은 전달되지 않은 것 으로 간주합니다. ‘발행’ 과 ‘전달’ 은 다르다 라는 BBS 의 인간적 진실을 컬럼 한 칸으로 박았어요.

Next

다음 편 — BBS 의 가장 사소해 보이는 두 줄 — GPS 위경도 — 가 어떻게 ‘책상 BBS’ 와 ‘현장 BBS’ 를 가르는지.